[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill-astronomer-airflow-plugins":3,"mdc--9hreme-key":55,"related-org-astronomer-airflow-plugins":4893,"related-repo-astronomer-airflow-plugins":5052},{"slug":4,"name":4,"fn":5,"description":6,"org":7,"tags":11,"stars":25,"repoUrl":26,"updatedAt":27,"license":28,"forks":29,"topics":30,"repo":50,"sourceUrl":53,"mdContent":54},"airflow-plugins","build Airflow UI 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.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},"astronomer","Astronomer","https:\u002F\u002Fpexgzepcugksgbtrxkhf.supabase.co\u002Fstorage\u002Fv1\u002Fobject\u002Fpublic\u002Forg-logos\u002Fastronomer.png",[12,16,19,22],{"name":13,"slug":14,"type":15},"Airflow","airflow","tag",{"name":17,"slug":18,"type":15},"Plugin Development","plugin-development",{"name":20,"slug":21,"type":15},"Python","python",{"name":23,"slug":24,"type":15},"UI Components","ui-components",412,"https:\u002F\u002Fgithub.com\u002Fastronomer\u002Fagents","2026-04-06T18:01:56.827891",null,55,[31,32,33,34,14,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49],"agentic-workflow","agents","ai","ai-agents","apache-airflow","claude","cursor","dag","data-engineering","data-pipelines","dbt","llm","mcp","orchestrator","skills","workflow-automation","workflow-management","workflow-orchestration","workflows",{"repoUrl":26,"stars":25,"forks":29,"topics":51,"description":52},[31,32,33,34,14,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49],"AI agent tooling for data engineering workflows.","https:\u002F\u002Fgithub.com\u002Fastronomer\u002Fagents\u002Ftree\u002FHEAD\u002Fskills\u002Fairflow-plugins","---\nname: airflow-plugins\ndescription: 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.\n---\n\n# Airflow 3 Plugins\n\nAirflow 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.\n\n> **CRITICAL**: Plugin components (fastapi_apps, react_apps, external_views) require **Airflow 3.1+**. **NEVER import `flask`, `flask_appbuilder`, or use `appbuilder_views` \u002F `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.\n>\n> **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.\n>\n> **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.\n>\n> **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.\n\n### Before writing any code, verify\n\n1. Am I using `fastapi_apps` \u002F FastAPI — not `appbuilder_views` \u002F Flask?\n2. Are all HTML\u002FJS asset paths and `fetch()` calls relative (no leading slash)?\n3. Are all synchronous SDK or SQLAlchemy calls wrapped in `asyncio.to_thread()`?\n4. Do the `static\u002F` and `assets\u002F` directories exist before the FastAPI app mounts them?\n5. If the endpoint must be private, did I add explicit FastAPI authentication?\n\n---\n\n## Step 1: Choose plugin components\n\nA single plugin class can register multiple component types at once.\n\n| Component | What it does | Field |\n|-----------|-------------|-------|\n| Custom API endpoints | FastAPI app mounted in Airflow process | `fastapi_apps` |\n| Nav \u002F page link | Embeds a URL as an iframe or links out | `external_views` |\n| React component | Custom React app embedded in Airflow UI | `react_apps` |\n| API middleware | Intercepts all Airflow API requests\u002Fresponses | `fastapi_root_middlewares` |\n| Jinja macros | Reusable Python functions in DAG templates | `macros` |\n| Task instance button | Extra link button in task Detail view | `operator_extra_links` \u002F `global_operator_extra_links` |\n| Custom timetable | Custom scheduling logic | `timetables` |\n| Event hooks | Listener callbacks for Airflow events | `listeners` |\n\n---\n\n## Step 2: Plugin registration skeleton\n\n### Project file structure\n\nGive each plugin its own subdirectory under `plugins\u002F` — this keeps the Python file, static assets, and templates together and makes multi-plugin projects manageable:\n\n```\nplugins\u002F\n  my-plugin\u002F\n    plugin.py       # AirflowPlugin subclass — auto-discovered by Airflow\n    static\u002F\n      index.html\n      app.js\n    assets\u002F\n      icon.svg\n```\n\n`BASE_DIR = Path(__file__).parent` in `plugin.py` resolves to `plugins\u002Fmy-plugin\u002F` — static and asset paths will be correct relative to that. Create the subdirectory and any static\u002Fassets folders before starting Airflow, or `StaticFiles` will raise on import.\n\n```python\nfrom pathlib import Path\nfrom airflow.plugins_manager import AirflowPlugin\nfrom fastapi import FastAPI\nfrom fastapi.staticfiles import StaticFiles\nfrom fastapi.responses import FileResponse\n\nBASE_DIR = Path(__file__).parent\n\napp = FastAPI(title=\"My Plugin\")\n\n# Both directories must exist before Airflow starts or FastAPI raises on import\napp.mount(\"\u002Fstatic\", StaticFiles(directory=BASE_DIR \u002F \"static\"), name=\"static\")\napp.mount(\"\u002Fassets\", StaticFiles(directory=BASE_DIR \u002F \"assets\"), name=\"assets\")\n\n\nclass MyPlugin(AirflowPlugin):\n    name = \"my_plugin\"\n\n    fastapi_apps = [\n        {\n            \"app\": app,\n            \"url_prefix\": \"\u002Fmy-plugin\",   # plugin available at {AIRFLOW_HOST}\u002Fmy-plugin\u002F\n            \"name\": \"My Plugin\",\n        }\n    ]\n\n    external_views = [\n        {\n            \"name\": \"My Plugin\",\n            \"href\": \"my-plugin\u002Fui\",              # NO leading slash — breaks on Astro and reverse proxies\n            \"destination\": \"nav\",                # see locations table below\n            \"category\": \"browse\",                # nav bar category (nav destination only)\n            \"url_route\": \"my-plugin\",            # unique route name (required for React apps)\n            \"icon\": \"\u002Fmy-plugin\u002Fstatic\u002Ficon.svg\" # DOES use a leading slash — served by FastAPI\n        }\n    ]\n```\n\n### External view locations\n\n| `destination` | Where it appears |\n|--------------|-----------------|\n| `\"nav\"` | Left navigation bar (also set `category`) |\n| `\"dag\"` | Extra tab on every Dag page |\n| `\"dag_run\"` | Extra tab on every Dag run page |\n| `\"task\"` | Extra tab on every task page |\n| `\"task_instance\"` | Extra tab on every task instance page |\n\n### Nav bar categories (`destination: \"nav\"`)\n\nSet `\"category\"` to place the link under a specific nav group: `\"browse\"`, `\"admin\"`, or omit for top-level.\n\n### External URLs and minimal plugins\n\n`href` can be a relative path to an internal endpoint (`\"my-plugin\u002Fui\"`) 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:\n\n```python\nfrom airflow.plugins_manager import AirflowPlugin\n\nclass LearnViewPlugin(AirflowPlugin):\n    name = \"learn_view_plugin\"\n\n    external_views = [\n        {\n            \"name\": \"Learn Airflow 3\",\n            \"href\": \"https:\u002F\u002Fwww.astronomer.io\u002Fdocs\u002Flearn\",\n            \"destination\": \"dag\",   # adds a tab to every Dag page\n            \"url_route\": \"learn\"\n        }\n    ]\n```\n\nThe no-leading-slash rule applies to internal paths only — full `https:\u002F\u002F` URLs are fine.\n\n---\n\n## Step 3: Serve the UI entry point\n\n```python\n@app.get(\"\u002Fui\", response_class=FileResponse)\nasync def serve_ui():\n    return FileResponse(BASE_DIR \u002F \"static\" \u002F \"index.html\")\n```\n\nIn HTML, always use **relative paths**. Absolute paths break when Airflow is mounted at a sub-path:\n\n```html\n\u003C!-- correct -->\n\u003Clink rel=\"stylesheet\" href=\"static\u002Fapp.css\" \u002F>\n\u003Cscript src=\"static\u002Fapp.js?v=20240315\">\u003C\u002Fscript>\n\n\u003C!-- breaks behind a reverse proxy -->\n\u003Cscript src=\"\u002Fmy-plugin\u002Fstatic\u002Fapp.js\">\u003C\u002Fscript>\n```\n\nSame rule in JavaScript:\n\n```javascript\nfetch('api\u002Fdags')           \u002F\u002F correct — relative to current page\nfetch('\u002Fmy-plugin\u002Fapi\u002Fdags') \u002F\u002F breaks on Astro and sub-path deploys\n```\n\n---\n\n## Step 4: Call the Airflow API from your plugin\n\n> **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.\n\n### Add the dependency\n\nOnly if REST API communication is being implemented: add `apache-airflow-client` to the project's dependencies. Check which file exists and act accordingly:\n\n| File found | Action |\n|------------|--------|\n| `requirements.txt` | Append `apache-airflow-client` |\n| `pyproject.toml` (uv \u002F poetry) | `uv add apache-airflow-client` or `poetry add apache-airflow-client` |\n| None of the above | Tell the user: \"Add `apache-airflow-client` to your dependencies before running the plugin.\" |\n\nUse `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.\n\n### JWT token management\n\nCache 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:\n\n> 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.\n\n```python\nimport asyncio\nimport os\nimport threading\nimport time\nimport airflow_client.client as airflow_sdk\nimport requests\n\nAIRFLOW_HOST  = os.environ.get(\"MYPLUGIN_HOST\",     \"http:\u002F\u002Flocalhost:8080\")\nAIRFLOW_USER  = os.environ.get(\"MYPLUGIN_USERNAME\", \"admin\")\nAIRFLOW_PASS  = os.environ.get(\"MYPLUGIN_PASSWORD\", \"admin\")\nAIRFLOW_TOKEN = os.environ.get(\"MYPLUGIN_TOKEN\")    # Astronomer Astro: Deployment API token\n\n_cached_token: str | None = None\n_token_expires_at: float  = 0.0\n_token_lock = threading.Lock()\n\n\ndef _fetch_fresh_token() -> str:\n    \"\"\"Exchange username\u002Fpassword for a JWT via Airflow's auth endpoint.\"\"\"\n    response = requests.post(\n        f\"{AIRFLOW_HOST}\u002Fauth\u002Ftoken\",\n        json={\"username\": AIRFLOW_USER, \"password\": AIRFLOW_PASS},\n        timeout=10,\n    )\n    response.raise_for_status()\n    return response.json()[\"access_token\"]\n\n\ndef _get_token() -> str:\n    # Astronomer Astro production: use static Deployment API token directly\n    if AIRFLOW_TOKEN:\n        return AIRFLOW_TOKEN\n    global _cached_token, _token_expires_at\n    now = time.monotonic()\n    # Fast path — no lock if still valid\n    if _cached_token and now \u003C _token_expires_at:\n        return _cached_token\n    # Slow path — one thread refreshes, others wait\n    with _token_lock:\n        if _cached_token and now \u003C _token_expires_at:\n            return _cached_token\n        _cached_token = _fetch_fresh_token()\n        _token_expires_at = now + 55 * 60  # refresh 5 min before 1-hour expiry\n    return _cached_token\n\n\ndef _make_config() -> airflow_sdk.Configuration:\n    config = airflow_sdk.Configuration(host=AIRFLOW_HOST)\n    config.access_token = _get_token()\n    return config\n```\n\nAfter implementing auth, tell the user:\n\n- **Local development**: set `MYPLUGIN_USERNAME` and `MYPLUGIN_PASSWORD` in `.env` — JWT exchange happens automatically.\n- **Astronomer Astro (production)**: create a Deployment API token and set it as `MYPLUGIN_TOKEN` — the JWT exchange is skipped entirely:\n  1. Astro UI → open the Deployment → **Access** → **API Tokens** → **+ Deployment API Token**\n  2. Copy the token value (shown only once)\n  3. `astro deployment variable create MYPLUGIN_TOKEN=\u003Ctoken>`\n\n  `MYPLUGIN_USERNAME` and `MYPLUGIN_PASSWORD` are not needed on Astro.\n\n### Wrapping SDK calls with asyncio.to_thread\n\n```python\nfrom fastapi import HTTPException\nfrom airflow_client.client.api import DAGApi\n\n@app.get(\"\u002Fapi\u002Fdags\")\nasync def list_dags():\n    try:\n        def _fetch():\n            with airflow_sdk.ApiClient(_make_config()) as client:\n                return DAGApi(client).get_dags(limit=100).dags\n        dags = await asyncio.to_thread(_fetch)\n        return [{\"dag_id\": d.dag_id, \"is_paused\": d.is_paused, \"timetable_summary\": d.timetable_summary} for d in dags]\n    except Exception as e:\n        raise HTTPException(status_code=500, detail=str(e))\n```\n\n> **API field names**: Never guess response field names — verify against the [REST API reference](https:\u002F\u002Fairflow.apache.org\u002Fdocs\u002Fapache-airflow\u002Fstable\u002Fstable-rest-api-ref.html). Key `DAGResponse` fields: `dag_id`, `dag_display_name`, `description`, `is_paused`, `timetable_summary`, `timetable_description`, `fileloc`, `owners`, `tags`.\n\nThe pattern is always: define a plain inner `def _fetch()` with all SDK logic, then `await asyncio.to_thread(_fetch)`.\n\n### Alternative: Direct database access\n\n> **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.\"\n\nSince 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.\n\n```python\nfrom airflow.models import DagBag, DagModel\nfrom airflow.utils.db import provide_session\n\n@app.get(\"\u002Fapi\u002Fdags\u002Fstatus\")\nasync def dag_status():\n    def _fetch():\n        @provide_session\n        def _query(session=None):\n            dagbag = DagBag()\n            paused = sum(\n                1 for dag_id in dagbag.dags\n                if (m := session.query(DagModel).filter(DagModel.dag_id == dag_id).first())\n                and m.is_paused\n            )\n            return {\"total\": len(dagbag.dags), \"paused\": paused}\n        return _query()\n    return await asyncio.to_thread(_fetch)\n```\n\n---\n\n## Step 5: Common API endpoint patterns\n\n> **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 \u003CClass>; print([m for m in dir(\u003CClass>) if not m.startswith('_')])\"` in any environment where the SDK is installed, or search the [`apache\u002Fairflow-client-python`](https:\u002F\u002Fgithub.com\u002Fapache\u002Fairflow-client-python) repo for the class definition.\n\n```python\nfrom airflow_client.client.api import DAGApi, DagRunApi\nfrom airflow_client.client.models import TriggerDAGRunPostBody, DAGPatchBody\n\n\n@app.post(\"\u002Fapi\u002Fdags\u002F{dag_id}\u002Ftrigger\")\nasync def trigger_dag(dag_id: str):\n    def _run():\n        with airflow_sdk.ApiClient(_make_config()) as client:\n            return DagRunApi(client).trigger_dag_run(dag_id, TriggerDAGRunPostBody())\n    result = await asyncio.to_thread(_run)\n    return {\"run_id\": result.dag_run_id, \"state\": normalize_state(result.state)}\n\n\n@app.patch(\"\u002Fapi\u002Fdags\u002F{dag_id}\u002Fpause\")\nasync def toggle_pause(dag_id: str, is_paused: bool):\n    def _run():\n        with airflow_sdk.ApiClient(_make_config()) as client:\n            DAGApi(client).patch_dag(dag_id, DAGPatchBody(is_paused=is_paused))\n    await asyncio.to_thread(_run)\n    return {\"dag_id\": dag_id, \"is_paused\": is_paused}\n\n\n@app.delete(\"\u002Fapi\u002Fdags\u002F{dag_id}\")\nasync def delete_dag(dag_id: str):\n    def _run():\n        with airflow_sdk.ApiClient(_make_config()) as client:\n            DAGApi(client).delete_dag(dag_id)\n    await asyncio.to_thread(_run)\n    return {\"deleted\": dag_id}\n\n\ndef normalize_state(raw) -> str:\n    \"\"\"Convert SDK enum objects to plain strings before sending to the frontend.\"\"\"\n    if raw is None:\n        return \"never_run\"\n    return str(raw).lower()\n```\n\n### DAG runs, task instances, and logs\n\nThese are the most common calls beyond basic DAG CRUD. For anything not shown here, consult the [REST API reference](https:\u002F\u002Fairflow.apache.org\u002Fdocs\u002Fapache-airflow\u002Fstable\u002Fstable-rest-api-ref.html) for available endpoints and the matching Python SDK class\u002Fmethod names.\n\n```python\nfrom airflow_client.client.api import DagRunApi, TaskInstanceApi\n\n# Latest run for a DAG\n@app.get(\"\u002Fapi\u002Fdags\u002F{dag_id}\u002Fruns\u002Flatest\")\nasync def latest_run(dag_id: str):\n    def _fetch():\n        with airflow_sdk.ApiClient(_make_config()) as client:\n            runs = DagRunApi(client).get_dag_runs(dag_id, limit=1, order_by=\"-start_date\").dag_runs\n            return runs[0] if runs else None\n    run = await asyncio.to_thread(_fetch)\n    if not run:\n        return {\"state\": \"never_run\"}\n    return {\"run_id\": run.dag_run_id, \"state\": normalize_state(run.state)}\n\n\n# Task instances for a specific run\n@app.get(\"\u002Fapi\u002Fdags\u002F{dag_id}\u002Fruns\u002F{run_id}\u002Ftasks\")\nasync def task_instances(dag_id: str, run_id: str):\n    def _fetch():\n        with airflow_sdk.ApiClient(_make_config()) as client:\n            return TaskInstanceApi(client).get_task_instances(dag_id, run_id).task_instances\n    tasks = await asyncio.to_thread(_fetch)\n    return [{\"task_id\": t.task_id, \"state\": normalize_state(t.state)} for t in tasks]\n\n\n# Task log (try_number starts at 1)\n@app.get(\"\u002Fapi\u002Fdags\u002F{dag_id}\u002Fruns\u002F{run_id}\u002Ftasks\u002F{task_id}\u002Flogs\u002F{try_number}\")\nasync def task_log(dag_id: str, run_id: str, task_id: str, try_number: int):\n    def _fetch():\n        with airflow_sdk.ApiClient(_make_config()) as client:\n            return TaskInstanceApi(client).get_log(\n                dag_id, run_id, task_id, try_number, map_index=-1\n            )\n    result = await asyncio.to_thread(_fetch)\n    return {\"log\": result.content if hasattr(result, \"content\") else str(result)}\n```\n\n### Streaming proxy\n\nUse `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.):\n\n```python\nimport requests\nfrom starlette.responses import StreamingResponse\n\n@app.get(\"\u002Fapi\u002Ffiles\u002F{filename}\")\nasync def proxy_file(filename: str):\n    def _stream():\n        r = requests.get(f\"https:\u002F\u002Ffiles.example.com\u002F{filename}\", stream=True)\n        r.raise_for_status()\n        return r\n    response = await asyncio.to_thread(_stream)\n    return StreamingResponse(\n        response.iter_content(chunk_size=8192),\n        media_type=\"application\u002Foctet-stream\",\n        headers={\"Content-Disposition\": f'attachment; filename=\"{filename}\"'},\n    )\n```\n\nNote that `requests.get()` is blocking — fetch in `asyncio.to_thread` so the event loop isn't stalled while waiting for the remote server.\n\n---\n\n## Step 6: Other plugin component types\n\n### Macros\n\nMacros are loaded by the **scheduler** (and DAG processor), not the API server. Restart the scheduler after changes.\n\n```python\nfrom airflow.plugins_manager import AirflowPlugin\n\ndef format_confidence(confidence: float) -> str:\n    return f\"{confidence * 100:.2f}%\"\n\nclass MyPlugin(AirflowPlugin):\n    name = \"my_plugin\"\n    macros = [format_confidence]\n```\n\nUse in any templated field — including with XCom:\n\n```\n{{ macros.my_plugin.format_confidence(0.95) }}\n\n{{ macros.my_plugin.format_confidence(ti.xcom_pull(task_ids='score_task')['confidence']) }}\n```\n\nThe naming pattern is always `macros.{plugin_name}.{function_name}`.\n\n### Middleware\n\nMiddleware applies to **all** Airflow API requests, including the built-in REST API and any FastAPI plugins. Use sparingly and filter requests explicitly if needed:\n\n```python\nfrom starlette.middleware.base import BaseHTTPMiddleware\nfrom fastapi import Request, Response\n\nclass AuditMiddleware(BaseHTTPMiddleware):\n    async def dispatch(self, request: Request, call_next) -> Response:\n        # runs before every request to the Airflow API server\n        response = await call_next(request)\n        return response\n\nclass MyPlugin(AirflowPlugin):\n    name = \"my_plugin\"\n    fastapi_root_middlewares = [\n        {\"middleware\": AuditMiddleware, \"args\": [], \"kwargs\": {}, \"name\": \"Audit\"}\n    ]\n```\n\n### Operator extra links\n\n```python\nfrom airflow.sdk.bases.operatorlink import BaseOperatorLink\n\nclass MyDashboardLink(BaseOperatorLink):\n    name = \"Open in Dashboard\"\n\n    def get_link(self, operator, *, ti_key, **context) -> str:\n        return f\"https:\u002F\u002Fmy-dashboard.example.com\u002Ftasks\u002F{ti_key.task_id}\"\n\nclass MyPlugin(AirflowPlugin):\n    name = \"my_plugin\"\n    global_operator_extra_links = [MyDashboardLink()]  # appears on every task\n    # operator_extra_links = [MyDashboardLink()]       # attach to specific operator instead\n```\n\n### React apps\n\nReact apps are embedded as JavaScript bundles served via FastAPI. The bundle must expose itself as a global variable matching the plugin name:\n\n```javascript\n\u002F\u002F In your bundle (e.g. my-app.js)\nglobalThis['My Plugin'] = MyComponent;   \u002F\u002F matches plugin name\nglobalThis.AirflowPlugin = MyComponent;  \u002F\u002F fallback Airflow looks for\n```\n\n```python\nclass MyPlugin(AirflowPlugin):\n    name = \"my_plugin\"\n    fastapi_apps = [{\"app\": app, \"url_prefix\": \"\u002Fmy-plugin\", \"name\": \"My Plugin\"}]\n    react_apps = [\n        {\n            \"name\": \"My Plugin\",\n            \"bundle_url\": \"\u002Fmy-plugin\u002Fmy-app.js\",\n            \"destination\": \"nav\",\n            \"category\": \"browse\",\n            \"url_route\": \"my-plugin\",\n        }\n    ]\n```\n\nThe same bundle can be registered to multiple destinations by adding multiple entries — each needs a unique `url_route`:\n\n```python\nreact_apps = [\n    {\"name\": \"My Widget\", \"bundle_url\": \"\u002Fmy-plugin\u002Fwidget.js\", \"destination\": \"nav\",  \"url_route\": \"my-widget-nav\"},\n    {\"name\": \"My Widget\", \"bundle_url\": \"\u002Fmy-plugin\u002Fwidget.js\", \"destination\": \"dag\",  \"url_route\": \"my-widget-dag\"},\n]\n```\n\n> React app integration is experimental in Airflow 3.1. Interfaces may change in future releases.\n\n---\n\n## Step 7: Environment variables and deployment\n\nNever hardcode credentials:\n\n```python\nAIRFLOW_HOST = os.environ.get(\"MYPLUGIN_HOST\",     \"http:\u002F\u002Flocalhost:8080\")\nAIRFLOW_USER = os.environ.get(\"MYPLUGIN_USERNAME\", \"admin\")\nAIRFLOW_PASS = os.environ.get(\"MYPLUGIN_PASSWORD\", \"admin\")\n```\n\n**Local Astro CLI:**\n```\n# .env\nMYPLUGIN_HOST=http:\u002F\u002Flocalhost:8080\nMYPLUGIN_USERNAME=admin\nMYPLUGIN_PASSWORD=admin\n```\n\n```bash\nastro dev restart              # required after any Python plugin change\n\n# Check logs by component (Astro CLI):\nastro dev logs --api-server    # FastAPI apps, external_views — plugin import errors show here\nastro dev logs --scheduler     # macros, timetables, listeners, operator links\nastro dev logs --dag-processor # DAG parsing errors\n\n# Non-Astro:\nairflow plugins                # CLI — lists all loaded plugins\n```\n\n**Production Astronomer:**\n```bash\nastro deployment variable create --deployment-id \u003Cid> MYPLUGIN_HOST=https:\u002F\u002Fairflow.example.com\n```\n\n**Auto-reload during development** (skips lazy loading):\n```\nAIRFLOW__CORE__LAZY_LOAD_PLUGINS=False\n```\n\n**Cache busting for static files** after deploy:\n```html\n\u003Cscript src=\"static\u002Fapp.js?v=20240315-1\">\u003C\u002Fscript>\n```\n\n**Verify the plugin loaded**: open **Admin > Plugins** in the Airflow UI.\n\n**OpenAPI docs** are auto-generated for FastAPI plugins:\n- Swagger UI: `{AIRFLOW_HOST}\u002F{url_prefix}\u002Fdocs`\n- OpenAPI JSON: `{AIRFLOW_HOST}\u002F{url_prefix}\u002Fopenapi.json`\n\n---\n\n## Common pitfalls\n\n| Problem | Cause | Fix |\n|---------|-------|-----|\n| Nav link goes to 404 | Leading `\u002F` in `href` | `\"my-plugin\u002Fui\"` not `\"\u002Fmy-plugin\u002Fui\"` |\n| Nav icon not showing | Missing `\u002F` in `icon` | `icon` takes an absolute path: `\"\u002Fmy-plugin\u002Fstatic\u002Ficon.svg\"` |\n| Event loop freezes under load | Sync SDK called directly in `async def` | Wrap with `asyncio.to_thread()` |\n| 401 errors after 1 hour | JWT expires with no refresh | Use the 5-minute pre-expiry refresh pattern |\n| `StaticFiles` raises on startup | Directory missing | Create `assets\u002F` and `static\u002F` before starting |\n| Plugin not showing up | Python file changed without restart | `astro dev restart` |\n| Endpoints accessible without login | FastAPI apps are not auto-authenticated | Add FastAPI security (e.g. OAuth2, API key) if endpoints must be private |\n| Middleware affecting wrong routes | Middleware applies to all API traffic | Filter by `request.url.path` inside `dispatch()` |\n| JS `fetch()` breaks on Astro | Absolute path in `fetch()` | Always use relative paths: `fetch('api\u002Fdags')` |\n\n---\n\n## References\n\n- [Airflow plugins documentation](https:\u002F\u002Fairflow.apache.org\u002Fdocs\u002Fapache-airflow\u002Fstable\u002Fadministration-and-deployment\u002Fplugins.html)\n- [Airflow REST API reference](https:\u002F\u002Fairflow.apache.org\u002Fdocs\u002Fapache-airflow\u002Fstable\u002Fstable-rest-api-ref.html) — full endpoint list with SDK class\u002Fmethod names\n- [Astronomer: Using Airflow plugins](https:\u002F\u002Fwww.astronomer.io\u002Fdocs\u002Flearn\u002Fusing-airflow-plugins)\n",{"data":56,"body":57},{"name":4,"description":6},{"type":58,"children":59},"root",[60,69,75,203,210,286,290,297,302,516,519,525,531,544,556,591,916,922,1041,1054,1082,1088,1120,1224,1237,1240,1246,1277,1289,1482,1487,1569,1572,1578,1598,1604,1617,1712,1739,1745,1750,1774,2187,2192,2303,2309,2419,2515,2535,2541,2561,2580,2722,2725,2731,2765,3047,3053,3065,3340,3346,3358,3482,3503,3506,3512,3517,3529,3595,3600,3609,3621,3627,3639,3753,3759,3857,3863,3868,3962,4059,4072,4111,4119,4122,4128,4133,4164,4172,4181,4337,4345,4404,4414,4423,4433,4484,4501,4511,4536,4539,4545,4844,4847,4853,4887],{"type":61,"tag":62,"props":63,"children":65},"element","h1",{"id":64},"airflow-3-plugins",[66],{"type":67,"value":68},"text","Airflow 3 Plugins",{"type":61,"tag":70,"props":71,"children":72},"p",{},[73],{"type":67,"value":74},"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.",{"type":61,"tag":76,"props":77,"children":78},"blockquote",{},[79,135,152,170],{"type":61,"tag":70,"props":80,"children":81},{},[82,88,90,95,97,133],{"type":61,"tag":83,"props":84,"children":85},"strong",{},[86],{"type":67,"value":87},"CRITICAL",{"type":67,"value":89},": Plugin components (fastapi_apps, react_apps, external_views) require ",{"type":61,"tag":83,"props":91,"children":92},{},[93],{"type":67,"value":94},"Airflow 3.1+",{"type":67,"value":96},". ",{"type":61,"tag":83,"props":98,"children":99},{},[100,102,109,111,117,119,125,127],{"type":67,"value":101},"NEVER import ",{"type":61,"tag":103,"props":104,"children":106},"code",{"className":105},[],[107],{"type":67,"value":108},"flask",{"type":67,"value":110},", ",{"type":61,"tag":103,"props":112,"children":114},{"className":113},[],[115],{"type":67,"value":116},"flask_appbuilder",{"type":67,"value":118},", or use ",{"type":61,"tag":103,"props":120,"children":122},{"className":121},[],[123],{"type":67,"value":124},"appbuilder_views",{"type":67,"value":126}," \u002F ",{"type":61,"tag":103,"props":128,"children":130},{"className":129},[],[131],{"type":67,"value":132},"flask_blueprints",{"type":67,"value":134}," — these are Airflow 2 patterns and will not work in Airflow 3. If existing code uses them, rewrite the entire registration block using FastAPI.",{"type":61,"tag":70,"props":136,"children":137},{},[138,143,145,150],{"type":61,"tag":83,"props":139,"children":140},{},[141],{"type":67,"value":142},"Security",{"type":67,"value":144},": FastAPI plugin endpoints are ",{"type":61,"tag":83,"props":146,"children":147},{},[148],{"type":67,"value":149},"not automatically protected",{"type":67,"value":151}," by Airflow auth. If your endpoints need to be private, implement authentication explicitly using FastAPI's security utilities.",{"type":61,"tag":70,"props":153,"children":154},{},[155,160,162,168],{"type":61,"tag":83,"props":156,"children":157},{},[158],{"type":67,"value":159},"Restart required",{"type":67,"value":161},": Changes to Python plugin files require restarting the API server. Static file changes (HTML, JS, CSS) are picked up immediately. Set ",{"type":61,"tag":103,"props":163,"children":165},{"className":164},[],[166],{"type":67,"value":167},"AIRFLOW__CORE__LAZY_LOAD_PLUGINS=False",{"type":67,"value":169}," during development to load plugins at startup rather than lazily.",{"type":61,"tag":70,"props":171,"children":172},{},[173,178,180,186,187,193,195,201],{"type":61,"tag":83,"props":174,"children":175},{},[176],{"type":67,"value":177},"Relative paths always",{"type":67,"value":179},": In ",{"type":61,"tag":103,"props":181,"children":183},{"className":182},[],[184],{"type":67,"value":185},"external_views",{"type":67,"value":110},{"type":61,"tag":103,"props":188,"children":190},{"className":189},[],[191],{"type":67,"value":192},"href",{"type":67,"value":194}," must have no leading slash. In HTML and JavaScript, use relative paths for all assets and ",{"type":61,"tag":103,"props":196,"children":198},{"className":197},[],[199],{"type":67,"value":200},"fetch()",{"type":67,"value":202}," calls. Absolute paths break behind reverse proxies.",{"type":61,"tag":204,"props":205,"children":207},"h3",{"id":206},"before-writing-any-code-verify",[208],{"type":67,"value":209},"Before writing any code, verify",{"type":61,"tag":211,"props":212,"children":213},"ol",{},[214,235,247,260,281],{"type":61,"tag":215,"props":216,"children":217},"li",{},[218,220,226,228,233],{"type":67,"value":219},"Am I using ",{"type":61,"tag":103,"props":221,"children":223},{"className":222},[],[224],{"type":67,"value":225},"fastapi_apps",{"type":67,"value":227}," \u002F FastAPI — not ",{"type":61,"tag":103,"props":229,"children":231},{"className":230},[],[232],{"type":67,"value":124},{"type":67,"value":234}," \u002F Flask?",{"type":61,"tag":215,"props":236,"children":237},{},[238,240,245],{"type":67,"value":239},"Are all HTML\u002FJS asset paths and ",{"type":61,"tag":103,"props":241,"children":243},{"className":242},[],[244],{"type":67,"value":200},{"type":67,"value":246}," calls relative (no leading slash)?",{"type":61,"tag":215,"props":248,"children":249},{},[250,252,258],{"type":67,"value":251},"Are all synchronous SDK or SQLAlchemy calls wrapped in ",{"type":61,"tag":103,"props":253,"children":255},{"className":254},[],[256],{"type":67,"value":257},"asyncio.to_thread()",{"type":67,"value":259},"?",{"type":61,"tag":215,"props":261,"children":262},{},[263,265,271,273,279],{"type":67,"value":264},"Do the ",{"type":61,"tag":103,"props":266,"children":268},{"className":267},[],[269],{"type":67,"value":270},"static\u002F",{"type":67,"value":272}," and ",{"type":61,"tag":103,"props":274,"children":276},{"className":275},[],[277],{"type":67,"value":278},"assets\u002F",{"type":67,"value":280}," directories exist before the FastAPI app mounts them?",{"type":61,"tag":215,"props":282,"children":283},{},[284],{"type":67,"value":285},"If the endpoint must be private, did I add explicit FastAPI authentication?",{"type":61,"tag":287,"props":288,"children":289},"hr",{},[],{"type":61,"tag":291,"props":292,"children":294},"h2",{"id":293},"step-1-choose-plugin-components",[295],{"type":67,"value":296},"Step 1: Choose plugin components",{"type":61,"tag":70,"props":298,"children":299},{},[300],{"type":67,"value":301},"A single plugin class can register multiple component types at once.",{"type":61,"tag":303,"props":304,"children":305},"table",{},[306,330],{"type":61,"tag":307,"props":308,"children":309},"thead",{},[310],{"type":61,"tag":311,"props":312,"children":313},"tr",{},[314,320,325],{"type":61,"tag":315,"props":316,"children":317},"th",{},[318],{"type":67,"value":319},"Component",{"type":61,"tag":315,"props":321,"children":322},{},[323],{"type":67,"value":324},"What it does",{"type":61,"tag":315,"props":326,"children":327},{},[328],{"type":67,"value":329},"Field",{"type":61,"tag":331,"props":332,"children":333},"tbody",{},[334,356,377,399,421,443,472,494],{"type":61,"tag":311,"props":335,"children":336},{},[337,343,348],{"type":61,"tag":338,"props":339,"children":340},"td",{},[341],{"type":67,"value":342},"Custom API endpoints",{"type":61,"tag":338,"props":344,"children":345},{},[346],{"type":67,"value":347},"FastAPI app mounted in Airflow process",{"type":61,"tag":338,"props":349,"children":350},{},[351],{"type":61,"tag":103,"props":352,"children":354},{"className":353},[],[355],{"type":67,"value":225},{"type":61,"tag":311,"props":357,"children":358},{},[359,364,369],{"type":61,"tag":338,"props":360,"children":361},{},[362],{"type":67,"value":363},"Nav \u002F page link",{"type":61,"tag":338,"props":365,"children":366},{},[367],{"type":67,"value":368},"Embeds a URL as an iframe or links out",{"type":61,"tag":338,"props":370,"children":371},{},[372],{"type":61,"tag":103,"props":373,"children":375},{"className":374},[],[376],{"type":67,"value":185},{"type":61,"tag":311,"props":378,"children":379},{},[380,385,390],{"type":61,"tag":338,"props":381,"children":382},{},[383],{"type":67,"value":384},"React component",{"type":61,"tag":338,"props":386,"children":387},{},[388],{"type":67,"value":389},"Custom React app embedded in Airflow UI",{"type":61,"tag":338,"props":391,"children":392},{},[393],{"type":61,"tag":103,"props":394,"children":396},{"className":395},[],[397],{"type":67,"value":398},"react_apps",{"type":61,"tag":311,"props":400,"children":401},{},[402,407,412],{"type":61,"tag":338,"props":403,"children":404},{},[405],{"type":67,"value":406},"API middleware",{"type":61,"tag":338,"props":408,"children":409},{},[410],{"type":67,"value":411},"Intercepts all Airflow API requests\u002Fresponses",{"type":61,"tag":338,"props":413,"children":414},{},[415],{"type":61,"tag":103,"props":416,"children":418},{"className":417},[],[419],{"type":67,"value":420},"fastapi_root_middlewares",{"type":61,"tag":311,"props":422,"children":423},{},[424,429,434],{"type":61,"tag":338,"props":425,"children":426},{},[427],{"type":67,"value":428},"Jinja macros",{"type":61,"tag":338,"props":430,"children":431},{},[432],{"type":67,"value":433},"Reusable Python functions in DAG templates",{"type":61,"tag":338,"props":435,"children":436},{},[437],{"type":61,"tag":103,"props":438,"children":440},{"className":439},[],[441],{"type":67,"value":442},"macros",{"type":61,"tag":311,"props":444,"children":445},{},[446,451,456],{"type":61,"tag":338,"props":447,"children":448},{},[449],{"type":67,"value":450},"Task instance button",{"type":61,"tag":338,"props":452,"children":453},{},[454],{"type":67,"value":455},"Extra link button in task Detail view",{"type":61,"tag":338,"props":457,"children":458},{},[459,465,466],{"type":61,"tag":103,"props":460,"children":462},{"className":461},[],[463],{"type":67,"value":464},"operator_extra_links",{"type":67,"value":126},{"type":61,"tag":103,"props":467,"children":469},{"className":468},[],[470],{"type":67,"value":471},"global_operator_extra_links",{"type":61,"tag":311,"props":473,"children":474},{},[475,480,485],{"type":61,"tag":338,"props":476,"children":477},{},[478],{"type":67,"value":479},"Custom timetable",{"type":61,"tag":338,"props":481,"children":482},{},[483],{"type":67,"value":484},"Custom scheduling logic",{"type":61,"tag":338,"props":486,"children":487},{},[488],{"type":61,"tag":103,"props":489,"children":491},{"className":490},[],[492],{"type":67,"value":493},"timetables",{"type":61,"tag":311,"props":495,"children":496},{},[497,502,507],{"type":61,"tag":338,"props":498,"children":499},{},[500],{"type":67,"value":501},"Event hooks",{"type":61,"tag":338,"props":503,"children":504},{},[505],{"type":67,"value":506},"Listener callbacks for Airflow events",{"type":61,"tag":338,"props":508,"children":509},{},[510],{"type":61,"tag":103,"props":511,"children":513},{"className":512},[],[514],{"type":67,"value":515},"listeners",{"type":61,"tag":287,"props":517,"children":518},{},[],{"type":61,"tag":291,"props":520,"children":522},{"id":521},"step-2-plugin-registration-skeleton",[523],{"type":67,"value":524},"Step 2: Plugin registration skeleton",{"type":61,"tag":204,"props":526,"children":528},{"id":527},"project-file-structure",[529],{"type":67,"value":530},"Project file structure",{"type":61,"tag":70,"props":532,"children":533},{},[534,536,542],{"type":67,"value":535},"Give each plugin its own subdirectory under ",{"type":61,"tag":103,"props":537,"children":539},{"className":538},[],[540],{"type":67,"value":541},"plugins\u002F",{"type":67,"value":543}," — this keeps the Python file, static assets, and templates together and makes multi-plugin projects manageable:",{"type":61,"tag":545,"props":546,"children":550},"pre",{"className":547,"code":549,"language":67},[548],"language-text","plugins\u002F\n  my-plugin\u002F\n    plugin.py       # AirflowPlugin subclass — auto-discovered by Airflow\n    static\u002F\n      index.html\n      app.js\n    assets\u002F\n      icon.svg\n",[551],{"type":61,"tag":103,"props":552,"children":554},{"__ignoreMap":553},"",[555],{"type":67,"value":549},{"type":61,"tag":70,"props":557,"children":558},{},[559,565,567,573,575,581,583,589],{"type":61,"tag":103,"props":560,"children":562},{"className":561},[],[563],{"type":67,"value":564},"BASE_DIR = Path(__file__).parent",{"type":67,"value":566}," in ",{"type":61,"tag":103,"props":568,"children":570},{"className":569},[],[571],{"type":67,"value":572},"plugin.py",{"type":67,"value":574}," resolves to ",{"type":61,"tag":103,"props":576,"children":578},{"className":577},[],[579],{"type":67,"value":580},"plugins\u002Fmy-plugin\u002F",{"type":67,"value":582}," — static and asset paths will be correct relative to that. Create the subdirectory and any static\u002Fassets folders before starting Airflow, or ",{"type":61,"tag":103,"props":584,"children":586},{"className":585},[],[587],{"type":67,"value":588},"StaticFiles",{"type":67,"value":590}," will raise on import.",{"type":61,"tag":545,"props":592,"children":595},{"className":593,"code":594,"language":21,"meta":553,"style":553},"language-python shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","from pathlib import Path\nfrom airflow.plugins_manager import AirflowPlugin\nfrom fastapi import FastAPI\nfrom fastapi.staticfiles import StaticFiles\nfrom fastapi.responses import FileResponse\n\nBASE_DIR = Path(__file__).parent\n\napp = FastAPI(title=\"My Plugin\")\n\n# Both directories must exist before Airflow starts or FastAPI raises on import\napp.mount(\"\u002Fstatic\", StaticFiles(directory=BASE_DIR \u002F \"static\"), name=\"static\")\napp.mount(\"\u002Fassets\", StaticFiles(directory=BASE_DIR \u002F \"assets\"), name=\"assets\")\n\n\nclass MyPlugin(AirflowPlugin):\n    name = \"my_plugin\"\n\n    fastapi_apps = [\n        {\n            \"app\": app,\n            \"url_prefix\": \"\u002Fmy-plugin\",   # plugin available at {AIRFLOW_HOST}\u002Fmy-plugin\u002F\n            \"name\": \"My Plugin\",\n        }\n    ]\n\n    external_views = [\n        {\n            \"name\": \"My Plugin\",\n            \"href\": \"my-plugin\u002Fui\",              # NO leading slash — breaks on Astro and reverse proxies\n            \"destination\": \"nav\",                # see locations table below\n            \"category\": \"browse\",                # nav bar category (nav destination only)\n            \"url_route\": \"my-plugin\",            # unique route name (required for React apps)\n            \"icon\": \"\u002Fmy-plugin\u002Fstatic\u002Ficon.svg\" # DOES use a leading slash — served by FastAPI\n        }\n    ]\n",[596],{"type":61,"tag":103,"props":597,"children":598},{"__ignoreMap":553},[599,610,619,628,637,646,656,665,673,682,690,699,708,717,725,733,742,751,759,768,777,786,795,804,813,822,830,839,847,855,864,873,882,891,900,908],{"type":61,"tag":600,"props":601,"children":604},"span",{"class":602,"line":603},"line",1,[605],{"type":61,"tag":600,"props":606,"children":607},{},[608],{"type":67,"value":609},"from pathlib import Path\n",{"type":61,"tag":600,"props":611,"children":613},{"class":602,"line":612},2,[614],{"type":61,"tag":600,"props":615,"children":616},{},[617],{"type":67,"value":618},"from airflow.plugins_manager import AirflowPlugin\n",{"type":61,"tag":600,"props":620,"children":622},{"class":602,"line":621},3,[623],{"type":61,"tag":600,"props":624,"children":625},{},[626],{"type":67,"value":627},"from fastapi import FastAPI\n",{"type":61,"tag":600,"props":629,"children":631},{"class":602,"line":630},4,[632],{"type":61,"tag":600,"props":633,"children":634},{},[635],{"type":67,"value":636},"from fastapi.staticfiles import StaticFiles\n",{"type":61,"tag":600,"props":638,"children":640},{"class":602,"line":639},5,[641],{"type":61,"tag":600,"props":642,"children":643},{},[644],{"type":67,"value":645},"from fastapi.responses import FileResponse\n",{"type":61,"tag":600,"props":647,"children":649},{"class":602,"line":648},6,[650],{"type":61,"tag":600,"props":651,"children":653},{"emptyLinePlaceholder":652},true,[654],{"type":67,"value":655},"\n",{"type":61,"tag":600,"props":657,"children":659},{"class":602,"line":658},7,[660],{"type":61,"tag":600,"props":661,"children":662},{},[663],{"type":67,"value":664},"BASE_DIR = Path(__file__).parent\n",{"type":61,"tag":600,"props":666,"children":668},{"class":602,"line":667},8,[669],{"type":61,"tag":600,"props":670,"children":671},{"emptyLinePlaceholder":652},[672],{"type":67,"value":655},{"type":61,"tag":600,"props":674,"children":676},{"class":602,"line":675},9,[677],{"type":61,"tag":600,"props":678,"children":679},{},[680],{"type":67,"value":681},"app = FastAPI(title=\"My Plugin\")\n",{"type":61,"tag":600,"props":683,"children":685},{"class":602,"line":684},10,[686],{"type":61,"tag":600,"props":687,"children":688},{"emptyLinePlaceholder":652},[689],{"type":67,"value":655},{"type":61,"tag":600,"props":691,"children":693},{"class":602,"line":692},11,[694],{"type":61,"tag":600,"props":695,"children":696},{},[697],{"type":67,"value":698},"# Both directories must exist before Airflow starts or FastAPI raises on import\n",{"type":61,"tag":600,"props":700,"children":702},{"class":602,"line":701},12,[703],{"type":61,"tag":600,"props":704,"children":705},{},[706],{"type":67,"value":707},"app.mount(\"\u002Fstatic\", StaticFiles(directory=BASE_DIR \u002F \"static\"), name=\"static\")\n",{"type":61,"tag":600,"props":709,"children":711},{"class":602,"line":710},13,[712],{"type":61,"tag":600,"props":713,"children":714},{},[715],{"type":67,"value":716},"app.mount(\"\u002Fassets\", StaticFiles(directory=BASE_DIR \u002F \"assets\"), name=\"assets\")\n",{"type":61,"tag":600,"props":718,"children":720},{"class":602,"line":719},14,[721],{"type":61,"tag":600,"props":722,"children":723},{"emptyLinePlaceholder":652},[724],{"type":67,"value":655},{"type":61,"tag":600,"props":726,"children":728},{"class":602,"line":727},15,[729],{"type":61,"tag":600,"props":730,"children":731},{"emptyLinePlaceholder":652},[732],{"type":67,"value":655},{"type":61,"tag":600,"props":734,"children":736},{"class":602,"line":735},16,[737],{"type":61,"tag":600,"props":738,"children":739},{},[740],{"type":67,"value":741},"class MyPlugin(AirflowPlugin):\n",{"type":61,"tag":600,"props":743,"children":745},{"class":602,"line":744},17,[746],{"type":61,"tag":600,"props":747,"children":748},{},[749],{"type":67,"value":750},"    name = \"my_plugin\"\n",{"type":61,"tag":600,"props":752,"children":754},{"class":602,"line":753},18,[755],{"type":61,"tag":600,"props":756,"children":757},{"emptyLinePlaceholder":652},[758],{"type":67,"value":655},{"type":61,"tag":600,"props":760,"children":762},{"class":602,"line":761},19,[763],{"type":61,"tag":600,"props":764,"children":765},{},[766],{"type":67,"value":767},"    fastapi_apps = [\n",{"type":61,"tag":600,"props":769,"children":771},{"class":602,"line":770},20,[772],{"type":61,"tag":600,"props":773,"children":774},{},[775],{"type":67,"value":776},"        {\n",{"type":61,"tag":600,"props":778,"children":780},{"class":602,"line":779},21,[781],{"type":61,"tag":600,"props":782,"children":783},{},[784],{"type":67,"value":785},"            \"app\": app,\n",{"type":61,"tag":600,"props":787,"children":789},{"class":602,"line":788},22,[790],{"type":61,"tag":600,"props":791,"children":792},{},[793],{"type":67,"value":794},"            \"url_prefix\": \"\u002Fmy-plugin\",   # plugin available at {AIRFLOW_HOST}\u002Fmy-plugin\u002F\n",{"type":61,"tag":600,"props":796,"children":798},{"class":602,"line":797},23,[799],{"type":61,"tag":600,"props":800,"children":801},{},[802],{"type":67,"value":803},"            \"name\": \"My Plugin\",\n",{"type":61,"tag":600,"props":805,"children":807},{"class":602,"line":806},24,[808],{"type":61,"tag":600,"props":809,"children":810},{},[811],{"type":67,"value":812},"        }\n",{"type":61,"tag":600,"props":814,"children":816},{"class":602,"line":815},25,[817],{"type":61,"tag":600,"props":818,"children":819},{},[820],{"type":67,"value":821},"    ]\n",{"type":61,"tag":600,"props":823,"children":825},{"class":602,"line":824},26,[826],{"type":61,"tag":600,"props":827,"children":828},{"emptyLinePlaceholder":652},[829],{"type":67,"value":655},{"type":61,"tag":600,"props":831,"children":833},{"class":602,"line":832},27,[834],{"type":61,"tag":600,"props":835,"children":836},{},[837],{"type":67,"value":838},"    external_views = [\n",{"type":61,"tag":600,"props":840,"children":842},{"class":602,"line":841},28,[843],{"type":61,"tag":600,"props":844,"children":845},{},[846],{"type":67,"value":776},{"type":61,"tag":600,"props":848,"children":850},{"class":602,"line":849},29,[851],{"type":61,"tag":600,"props":852,"children":853},{},[854],{"type":67,"value":803},{"type":61,"tag":600,"props":856,"children":858},{"class":602,"line":857},30,[859],{"type":61,"tag":600,"props":860,"children":861},{},[862],{"type":67,"value":863},"            \"href\": \"my-plugin\u002Fui\",              # NO leading slash — breaks on Astro and reverse proxies\n",{"type":61,"tag":600,"props":865,"children":867},{"class":602,"line":866},31,[868],{"type":61,"tag":600,"props":869,"children":870},{},[871],{"type":67,"value":872},"            \"destination\": \"nav\",                # see locations table below\n",{"type":61,"tag":600,"props":874,"children":876},{"class":602,"line":875},32,[877],{"type":61,"tag":600,"props":878,"children":879},{},[880],{"type":67,"value":881},"            \"category\": \"browse\",                # nav bar category (nav destination only)\n",{"type":61,"tag":600,"props":883,"children":885},{"class":602,"line":884},33,[886],{"type":61,"tag":600,"props":887,"children":888},{},[889],{"type":67,"value":890},"            \"url_route\": \"my-plugin\",            # unique route name (required for React apps)\n",{"type":61,"tag":600,"props":892,"children":894},{"class":602,"line":893},34,[895],{"type":61,"tag":600,"props":896,"children":897},{},[898],{"type":67,"value":899},"            \"icon\": \"\u002Fmy-plugin\u002Fstatic\u002Ficon.svg\" # DOES use a leading slash — served by FastAPI\n",{"type":61,"tag":600,"props":901,"children":903},{"class":602,"line":902},35,[904],{"type":61,"tag":600,"props":905,"children":906},{},[907],{"type":67,"value":812},{"type":61,"tag":600,"props":909,"children":911},{"class":602,"line":910},36,[912],{"type":61,"tag":600,"props":913,"children":914},{},[915],{"type":67,"value":821},{"type":61,"tag":204,"props":917,"children":919},{"id":918},"external-view-locations",[920],{"type":67,"value":921},"External view locations",{"type":61,"tag":303,"props":923,"children":924},{},[925,945],{"type":61,"tag":307,"props":926,"children":927},{},[928],{"type":61,"tag":311,"props":929,"children":930},{},[931,940],{"type":61,"tag":315,"props":932,"children":933},{},[934],{"type":61,"tag":103,"props":935,"children":937},{"className":936},[],[938],{"type":67,"value":939},"destination",{"type":61,"tag":315,"props":941,"children":942},{},[943],{"type":67,"value":944},"Where it appears",{"type":61,"tag":331,"props":946,"children":947},{},[948,973,990,1007,1024],{"type":61,"tag":311,"props":949,"children":950},{},[951,960],{"type":61,"tag":338,"props":952,"children":953},{},[954],{"type":61,"tag":103,"props":955,"children":957},{"className":956},[],[958],{"type":67,"value":959},"\"nav\"",{"type":61,"tag":338,"props":961,"children":962},{},[963,965,971],{"type":67,"value":964},"Left navigation bar (also set ",{"type":61,"tag":103,"props":966,"children":968},{"className":967},[],[969],{"type":67,"value":970},"category",{"type":67,"value":972},")",{"type":61,"tag":311,"props":974,"children":975},{},[976,985],{"type":61,"tag":338,"props":977,"children":978},{},[979],{"type":61,"tag":103,"props":980,"children":982},{"className":981},[],[983],{"type":67,"value":984},"\"dag\"",{"type":61,"tag":338,"props":986,"children":987},{},[988],{"type":67,"value":989},"Extra tab on every Dag page",{"type":61,"tag":311,"props":991,"children":992},{},[993,1002],{"type":61,"tag":338,"props":994,"children":995},{},[996],{"type":61,"tag":103,"props":997,"children":999},{"className":998},[],[1000],{"type":67,"value":1001},"\"dag_run\"",{"type":61,"tag":338,"props":1003,"children":1004},{},[1005],{"type":67,"value":1006},"Extra tab on every Dag run page",{"type":61,"tag":311,"props":1008,"children":1009},{},[1010,1019],{"type":61,"tag":338,"props":1011,"children":1012},{},[1013],{"type":61,"tag":103,"props":1014,"children":1016},{"className":1015},[],[1017],{"type":67,"value":1018},"\"task\"",{"type":61,"tag":338,"props":1020,"children":1021},{},[1022],{"type":67,"value":1023},"Extra tab on every task page",{"type":61,"tag":311,"props":1025,"children":1026},{},[1027,1036],{"type":61,"tag":338,"props":1028,"children":1029},{},[1030],{"type":61,"tag":103,"props":1031,"children":1033},{"className":1032},[],[1034],{"type":67,"value":1035},"\"task_instance\"",{"type":61,"tag":338,"props":1037,"children":1038},{},[1039],{"type":67,"value":1040},"Extra tab on every task instance page",{"type":61,"tag":204,"props":1042,"children":1044},{"id":1043},"nav-bar-categories-destination-nav",[1045,1047,1053],{"type":67,"value":1046},"Nav bar categories (",{"type":61,"tag":103,"props":1048,"children":1050},{"className":1049},[],[1051],{"type":67,"value":1052},"destination: \"nav\"",{"type":67,"value":972},{"type":61,"tag":70,"props":1055,"children":1056},{},[1057,1059,1065,1067,1073,1074,1080],{"type":67,"value":1058},"Set ",{"type":61,"tag":103,"props":1060,"children":1062},{"className":1061},[],[1063],{"type":67,"value":1064},"\"category\"",{"type":67,"value":1066}," to place the link under a specific nav group: ",{"type":61,"tag":103,"props":1068,"children":1070},{"className":1069},[],[1071],{"type":67,"value":1072},"\"browse\"",{"type":67,"value":110},{"type":61,"tag":103,"props":1075,"children":1077},{"className":1076},[],[1078],{"type":67,"value":1079},"\"admin\"",{"type":67,"value":1081},", or omit for top-level.",{"type":61,"tag":204,"props":1083,"children":1085},{"id":1084},"external-urls-and-minimal-plugins",[1086],{"type":67,"value":1087},"External URLs and minimal plugins",{"type":61,"tag":70,"props":1089,"children":1090},{},[1091,1096,1098,1104,1106,1111,1113,1118],{"type":61,"tag":103,"props":1092,"children":1094},{"className":1093},[],[1095],{"type":67,"value":192},{"type":67,"value":1097}," can be a relative path to an internal endpoint (",{"type":61,"tag":103,"props":1099,"children":1101},{"className":1100},[],[1102],{"type":67,"value":1103},"\"my-plugin\u002Fui\"",{"type":67,"value":1105},") or a full external URL. A plugin with only ",{"type":61,"tag":103,"props":1107,"children":1109},{"className":1108},[],[1110],{"type":67,"value":185},{"type":67,"value":1112}," and no ",{"type":61,"tag":103,"props":1114,"children":1116},{"className":1115},[],[1117],{"type":67,"value":225},{"type":67,"value":1119}," is valid — no backend needed for a simple link or tab:",{"type":61,"tag":545,"props":1121,"children":1123},{"className":593,"code":1122,"language":21,"meta":553,"style":553},"from airflow.plugins_manager import AirflowPlugin\n\nclass LearnViewPlugin(AirflowPlugin):\n    name = \"learn_view_plugin\"\n\n    external_views = [\n        {\n            \"name\": \"Learn Airflow 3\",\n            \"href\": \"https:\u002F\u002Fwww.astronomer.io\u002Fdocs\u002Flearn\",\n            \"destination\": \"dag\",   # adds a tab to every Dag page\n            \"url_route\": \"learn\"\n        }\n    ]\n",[1124],{"type":61,"tag":103,"props":1125,"children":1126},{"__ignoreMap":553},[1127,1134,1141,1149,1157,1164,1171,1178,1186,1194,1202,1210,1217],{"type":61,"tag":600,"props":1128,"children":1129},{"class":602,"line":603},[1130],{"type":61,"tag":600,"props":1131,"children":1132},{},[1133],{"type":67,"value":618},{"type":61,"tag":600,"props":1135,"children":1136},{"class":602,"line":612},[1137],{"type":61,"tag":600,"props":1138,"children":1139},{"emptyLinePlaceholder":652},[1140],{"type":67,"value":655},{"type":61,"tag":600,"props":1142,"children":1143},{"class":602,"line":621},[1144],{"type":61,"tag":600,"props":1145,"children":1146},{},[1147],{"type":67,"value":1148},"class LearnViewPlugin(AirflowPlugin):\n",{"type":61,"tag":600,"props":1150,"children":1151},{"class":602,"line":630},[1152],{"type":61,"tag":600,"props":1153,"children":1154},{},[1155],{"type":67,"value":1156},"    name = \"learn_view_plugin\"\n",{"type":61,"tag":600,"props":1158,"children":1159},{"class":602,"line":639},[1160],{"type":61,"tag":600,"props":1161,"children":1162},{"emptyLinePlaceholder":652},[1163],{"type":67,"value":655},{"type":61,"tag":600,"props":1165,"children":1166},{"class":602,"line":648},[1167],{"type":61,"tag":600,"props":1168,"children":1169},{},[1170],{"type":67,"value":838},{"type":61,"tag":600,"props":1172,"children":1173},{"class":602,"line":658},[1174],{"type":61,"tag":600,"props":1175,"children":1176},{},[1177],{"type":67,"value":776},{"type":61,"tag":600,"props":1179,"children":1180},{"class":602,"line":667},[1181],{"type":61,"tag":600,"props":1182,"children":1183},{},[1184],{"type":67,"value":1185},"            \"name\": \"Learn Airflow 3\",\n",{"type":61,"tag":600,"props":1187,"children":1188},{"class":602,"line":675},[1189],{"type":61,"tag":600,"props":1190,"children":1191},{},[1192],{"type":67,"value":1193},"            \"href\": \"https:\u002F\u002Fwww.astronomer.io\u002Fdocs\u002Flearn\",\n",{"type":61,"tag":600,"props":1195,"children":1196},{"class":602,"line":684},[1197],{"type":61,"tag":600,"props":1198,"children":1199},{},[1200],{"type":67,"value":1201},"            \"destination\": \"dag\",   # adds a tab to every Dag page\n",{"type":61,"tag":600,"props":1203,"children":1204},{"class":602,"line":692},[1205],{"type":61,"tag":600,"props":1206,"children":1207},{},[1208],{"type":67,"value":1209},"            \"url_route\": \"learn\"\n",{"type":61,"tag":600,"props":1211,"children":1212},{"class":602,"line":701},[1213],{"type":61,"tag":600,"props":1214,"children":1215},{},[1216],{"type":67,"value":812},{"type":61,"tag":600,"props":1218,"children":1219},{"class":602,"line":710},[1220],{"type":61,"tag":600,"props":1221,"children":1222},{},[1223],{"type":67,"value":821},{"type":61,"tag":70,"props":1225,"children":1226},{},[1227,1229,1235],{"type":67,"value":1228},"The no-leading-slash rule applies to internal paths only — full ",{"type":61,"tag":103,"props":1230,"children":1232},{"className":1231},[],[1233],{"type":67,"value":1234},"https:\u002F\u002F",{"type":67,"value":1236}," URLs are fine.",{"type":61,"tag":287,"props":1238,"children":1239},{},[],{"type":61,"tag":291,"props":1241,"children":1243},{"id":1242},"step-3-serve-the-ui-entry-point",[1244],{"type":67,"value":1245},"Step 3: Serve the UI entry point",{"type":61,"tag":545,"props":1247,"children":1249},{"className":593,"code":1248,"language":21,"meta":553,"style":553},"@app.get(\"\u002Fui\", response_class=FileResponse)\nasync def serve_ui():\n    return FileResponse(BASE_DIR \u002F \"static\" \u002F \"index.html\")\n",[1250],{"type":61,"tag":103,"props":1251,"children":1252},{"__ignoreMap":553},[1253,1261,1269],{"type":61,"tag":600,"props":1254,"children":1255},{"class":602,"line":603},[1256],{"type":61,"tag":600,"props":1257,"children":1258},{},[1259],{"type":67,"value":1260},"@app.get(\"\u002Fui\", response_class=FileResponse)\n",{"type":61,"tag":600,"props":1262,"children":1263},{"class":602,"line":612},[1264],{"type":61,"tag":600,"props":1265,"children":1266},{},[1267],{"type":67,"value":1268},"async def serve_ui():\n",{"type":61,"tag":600,"props":1270,"children":1271},{"class":602,"line":621},[1272],{"type":61,"tag":600,"props":1273,"children":1274},{},[1275],{"type":67,"value":1276},"    return FileResponse(BASE_DIR \u002F \"static\" \u002F \"index.html\")\n",{"type":61,"tag":70,"props":1278,"children":1279},{},[1280,1282,1287],{"type":67,"value":1281},"In HTML, always use ",{"type":61,"tag":83,"props":1283,"children":1284},{},[1285],{"type":67,"value":1286},"relative paths",{"type":67,"value":1288},". Absolute paths break when Airflow is mounted at a sub-path:",{"type":61,"tag":545,"props":1290,"children":1294},{"className":1291,"code":1292,"language":1293,"meta":553,"style":553},"language-html shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003C!-- correct -->\n\u003Clink rel=\"stylesheet\" href=\"static\u002Fapp.css\" \u002F>\n\u003Cscript src=\"static\u002Fapp.js?v=20240315\">\u003C\u002Fscript>\n\n\u003C!-- breaks behind a reverse proxy -->\n\u003Cscript src=\"\u002Fmy-plugin\u002Fstatic\u002Fapp.js\">\u003C\u002Fscript>\n","html",[1295],{"type":61,"tag":103,"props":1296,"children":1297},{"__ignoreMap":553},[1298,1307,1375,1423,1430,1438],{"type":61,"tag":600,"props":1299,"children":1300},{"class":602,"line":603},[1301],{"type":61,"tag":600,"props":1302,"children":1304},{"style":1303},"--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic",[1305],{"type":67,"value":1306},"\u003C!-- correct -->\n",{"type":61,"tag":600,"props":1308,"children":1309},{"class":602,"line":612},[1310,1316,1322,1328,1333,1338,1344,1348,1353,1357,1361,1366,1370],{"type":61,"tag":600,"props":1311,"children":1313},{"style":1312},"--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF",[1314],{"type":67,"value":1315},"\u003C",{"type":61,"tag":600,"props":1317,"children":1319},{"style":1318},"--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178",[1320],{"type":67,"value":1321},"link",{"type":61,"tag":600,"props":1323,"children":1325},{"style":1324},"--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA",[1326],{"type":67,"value":1327}," rel",{"type":61,"tag":600,"props":1329,"children":1330},{"style":1312},[1331],{"type":67,"value":1332},"=",{"type":61,"tag":600,"props":1334,"children":1335},{"style":1312},[1336],{"type":67,"value":1337},"\"",{"type":61,"tag":600,"props":1339,"children":1341},{"style":1340},"--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D",[1342],{"type":67,"value":1343},"stylesheet",{"type":61,"tag":600,"props":1345,"children":1346},{"style":1312},[1347],{"type":67,"value":1337},{"type":61,"tag":600,"props":1349,"children":1350},{"style":1324},[1351],{"type":67,"value":1352}," href",{"type":61,"tag":600,"props":1354,"children":1355},{"style":1312},[1356],{"type":67,"value":1332},{"type":61,"tag":600,"props":1358,"children":1359},{"style":1312},[1360],{"type":67,"value":1337},{"type":61,"tag":600,"props":1362,"children":1363},{"style":1340},[1364],{"type":67,"value":1365},"static\u002Fapp.css",{"type":61,"tag":600,"props":1367,"children":1368},{"style":1312},[1369],{"type":67,"value":1337},{"type":61,"tag":600,"props":1371,"children":1372},{"style":1312},[1373],{"type":67,"value":1374}," \u002F>\n",{"type":61,"tag":600,"props":1376,"children":1377},{"class":602,"line":621},[1378,1382,1387,1392,1396,1400,1405,1409,1414,1418],{"type":61,"tag":600,"props":1379,"children":1380},{"style":1312},[1381],{"type":67,"value":1315},{"type":61,"tag":600,"props":1383,"children":1384},{"style":1318},[1385],{"type":67,"value":1386},"script",{"type":61,"tag":600,"props":1388,"children":1389},{"style":1324},[1390],{"type":67,"value":1391}," src",{"type":61,"tag":600,"props":1393,"children":1394},{"style":1312},[1395],{"type":67,"value":1332},{"type":61,"tag":600,"props":1397,"children":1398},{"style":1312},[1399],{"type":67,"value":1337},{"type":61,"tag":600,"props":1401,"children":1402},{"style":1340},[1403],{"type":67,"value":1404},"static\u002Fapp.js?v=20240315",{"type":61,"tag":600,"props":1406,"children":1407},{"style":1312},[1408],{"type":67,"value":1337},{"type":61,"tag":600,"props":1410,"children":1411},{"style":1312},[1412],{"type":67,"value":1413},">\u003C\u002F",{"type":61,"tag":600,"props":1415,"children":1416},{"style":1318},[1417],{"type":67,"value":1386},{"type":61,"tag":600,"props":1419,"children":1420},{"style":1312},[1421],{"type":67,"value":1422},">\n",{"type":61,"tag":600,"props":1424,"children":1425},{"class":602,"line":630},[1426],{"type":61,"tag":600,"props":1427,"children":1428},{"emptyLinePlaceholder":652},[1429],{"type":67,"value":655},{"type":61,"tag":600,"props":1431,"children":1432},{"class":602,"line":639},[1433],{"type":61,"tag":600,"props":1434,"children":1435},{"style":1303},[1436],{"type":67,"value":1437},"\u003C!-- breaks behind a reverse proxy -->\n",{"type":61,"tag":600,"props":1439,"children":1440},{"class":602,"line":648},[1441,1445,1449,1453,1457,1461,1466,1470,1474,1478],{"type":61,"tag":600,"props":1442,"children":1443},{"style":1312},[1444],{"type":67,"value":1315},{"type":61,"tag":600,"props":1446,"children":1447},{"style":1318},[1448],{"type":67,"value":1386},{"type":61,"tag":600,"props":1450,"children":1451},{"style":1324},[1452],{"type":67,"value":1391},{"type":61,"tag":600,"props":1454,"children":1455},{"style":1312},[1456],{"type":67,"value":1332},{"type":61,"tag":600,"props":1458,"children":1459},{"style":1312},[1460],{"type":67,"value":1337},{"type":61,"tag":600,"props":1462,"children":1463},{"style":1340},[1464],{"type":67,"value":1465},"\u002Fmy-plugin\u002Fstatic\u002Fapp.js",{"type":61,"tag":600,"props":1467,"children":1468},{"style":1312},[1469],{"type":67,"value":1337},{"type":61,"tag":600,"props":1471,"children":1472},{"style":1312},[1473],{"type":67,"value":1413},{"type":61,"tag":600,"props":1475,"children":1476},{"style":1318},[1477],{"type":67,"value":1386},{"type":61,"tag":600,"props":1479,"children":1480},{"style":1312},[1481],{"type":67,"value":1422},{"type":61,"tag":70,"props":1483,"children":1484},{},[1485],{"type":67,"value":1486},"Same rule in JavaScript:",{"type":61,"tag":545,"props":1488,"children":1492},{"className":1489,"code":1490,"language":1491,"meta":553,"style":553},"language-javascript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","fetch('api\u002Fdags')           \u002F\u002F correct — relative to current page\nfetch('\u002Fmy-plugin\u002Fapi\u002Fdags') \u002F\u002F breaks on Astro and sub-path deploys\n","javascript",[1493],{"type":61,"tag":103,"props":1494,"children":1495},{"__ignoreMap":553},[1496,1535],{"type":61,"tag":600,"props":1497,"children":1498},{"class":602,"line":603},[1499,1505,1511,1516,1521,1525,1530],{"type":61,"tag":600,"props":1500,"children":1502},{"style":1501},"--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF",[1503],{"type":67,"value":1504},"fetch",{"type":61,"tag":600,"props":1506,"children":1508},{"style":1507},"--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8",[1509],{"type":67,"value":1510},"(",{"type":61,"tag":600,"props":1512,"children":1513},{"style":1312},[1514],{"type":67,"value":1515},"'",{"type":61,"tag":600,"props":1517,"children":1518},{"style":1340},[1519],{"type":67,"value":1520},"api\u002Fdags",{"type":61,"tag":600,"props":1522,"children":1523},{"style":1312},[1524],{"type":67,"value":1515},{"type":61,"tag":600,"props":1526,"children":1527},{"style":1507},[1528],{"type":67,"value":1529},")           ",{"type":61,"tag":600,"props":1531,"children":1532},{"style":1303},[1533],{"type":67,"value":1534},"\u002F\u002F correct — relative to current page\n",{"type":61,"tag":600,"props":1536,"children":1537},{"class":602,"line":612},[1538,1542,1546,1550,1555,1559,1564],{"type":61,"tag":600,"props":1539,"children":1540},{"style":1501},[1541],{"type":67,"value":1504},{"type":61,"tag":600,"props":1543,"children":1544},{"style":1507},[1545],{"type":67,"value":1510},{"type":61,"tag":600,"props":1547,"children":1548},{"style":1312},[1549],{"type":67,"value":1515},{"type":61,"tag":600,"props":1551,"children":1552},{"style":1340},[1553],{"type":67,"value":1554},"\u002Fmy-plugin\u002Fapi\u002Fdags",{"type":61,"tag":600,"props":1556,"children":1557},{"style":1312},[1558],{"type":67,"value":1515},{"type":61,"tag":600,"props":1560,"children":1561},{"style":1507},[1562],{"type":67,"value":1563},") ",{"type":61,"tag":600,"props":1565,"children":1566},{"style":1303},[1567],{"type":67,"value":1568},"\u002F\u002F breaks on Astro and sub-path deploys\n",{"type":61,"tag":287,"props":1570,"children":1571},{},[],{"type":61,"tag":291,"props":1573,"children":1575},{"id":1574},"step-4-call-the-airflow-api-from-your-plugin",[1576],{"type":67,"value":1577},"Step 4: Call the Airflow API from your plugin",{"type":61,"tag":76,"props":1579,"children":1580},{},[1581],{"type":61,"tag":70,"props":1582,"children":1583},{},[1584,1589,1591,1596],{"type":61,"tag":83,"props":1585,"children":1586},{},[1587],{"type":67,"value":1588},"Only needed if your plugin calls the Airflow REST API.",{"type":67,"value":1590}," Plugins that only serve static files, register ",{"type":61,"tag":103,"props":1592,"children":1594},{"className":1593},[],[1595],{"type":67,"value":185},{"type":67,"value":1597},", or use direct DB access do not need this step — skip to Step 5 or Step 6.",{"type":61,"tag":204,"props":1599,"children":1601},{"id":1600},"add-the-dependency",[1602],{"type":67,"value":1603},"Add the dependency",{"type":61,"tag":70,"props":1605,"children":1606},{},[1607,1609,1615],{"type":67,"value":1608},"Only if REST API communication is being implemented: add ",{"type":61,"tag":103,"props":1610,"children":1612},{"className":1611},[],[1613],{"type":67,"value":1614},"apache-airflow-client",{"type":67,"value":1616}," to the project's dependencies. Check which file exists and act accordingly:",{"type":61,"tag":303,"props":1618,"children":1619},{},[1620,1636],{"type":61,"tag":307,"props":1621,"children":1622},{},[1623],{"type":61,"tag":311,"props":1624,"children":1625},{},[1626,1631],{"type":61,"tag":315,"props":1627,"children":1628},{},[1629],{"type":67,"value":1630},"File found",{"type":61,"tag":315,"props":1632,"children":1633},{},[1634],{"type":67,"value":1635},"Action",{"type":61,"tag":331,"props":1637,"children":1638},{},[1639,1661,1692],{"type":61,"tag":311,"props":1640,"children":1641},{},[1642,1651],{"type":61,"tag":338,"props":1643,"children":1644},{},[1645],{"type":61,"tag":103,"props":1646,"children":1648},{"className":1647},[],[1649],{"type":67,"value":1650},"requirements.txt",{"type":61,"tag":338,"props":1652,"children":1653},{},[1654,1656],{"type":67,"value":1655},"Append ",{"type":61,"tag":103,"props":1657,"children":1659},{"className":1658},[],[1660],{"type":67,"value":1614},{"type":61,"tag":311,"props":1662,"children":1663},{},[1664,1675],{"type":61,"tag":338,"props":1665,"children":1666},{},[1667,1673],{"type":61,"tag":103,"props":1668,"children":1670},{"className":1669},[],[1671],{"type":67,"value":1672},"pyproject.toml",{"type":67,"value":1674}," (uv \u002F poetry)",{"type":61,"tag":338,"props":1676,"children":1677},{},[1678,1684,1686],{"type":61,"tag":103,"props":1679,"children":1681},{"className":1680},[],[1682],{"type":67,"value":1683},"uv add apache-airflow-client",{"type":67,"value":1685}," or ",{"type":61,"tag":103,"props":1687,"children":1689},{"className":1688},[],[1690],{"type":67,"value":1691},"poetry add apache-airflow-client",{"type":61,"tag":311,"props":1693,"children":1694},{},[1695,1700],{"type":61,"tag":338,"props":1696,"children":1697},{},[1698],{"type":67,"value":1699},"None of the above",{"type":61,"tag":338,"props":1701,"children":1702},{},[1703,1705,1710],{"type":67,"value":1704},"Tell the user: \"Add ",{"type":61,"tag":103,"props":1706,"children":1708},{"className":1707},[],[1709],{"type":67,"value":1614},{"type":67,"value":1711}," to your dependencies before running the plugin.\"",{"type":61,"tag":70,"props":1713,"children":1714},{},[1715,1717,1722,1724,1729,1731,1737],{"type":67,"value":1716},"Use ",{"type":61,"tag":103,"props":1718,"children":1720},{"className":1719},[],[1721],{"type":67,"value":1614},{"type":67,"value":1723}," to talk to Airflow's own REST API. The SDK is ",{"type":61,"tag":83,"props":1725,"children":1726},{},[1727],{"type":67,"value":1728},"synchronous",{"type":67,"value":1730}," but FastAPI routes are async — never call blocking SDK methods directly inside ",{"type":61,"tag":103,"props":1732,"children":1734},{"className":1733},[],[1735],{"type":67,"value":1736},"async def",{"type":67,"value":1738}," or you will stall the event loop and freeze all concurrent requests.",{"type":61,"tag":204,"props":1740,"children":1742},{"id":1741},"jwt-token-management",[1743],{"type":67,"value":1744},"JWT token management",{"type":61,"tag":70,"props":1746,"children":1747},{},[1748],{"type":67,"value":1749},"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:",{"type":61,"tag":76,"props":1751,"children":1752},{},[1753],{"type":61,"tag":70,"props":1754,"children":1755},{},[1756,1758,1764,1766,1772],{"type":67,"value":1757},"Replace ",{"type":61,"tag":103,"props":1759,"children":1761},{"className":1760},[],[1762],{"type":67,"value":1763},"MYPLUGIN_",{"type":67,"value":1765}," with a short uppercase prefix derived from the plugin name (e.g. if the plugin is called \"Trip Analyzer\", use ",{"type":61,"tag":103,"props":1767,"children":1769},{"className":1768},[],[1770],{"type":67,"value":1771},"TRIP_ANALYZER_",{"type":67,"value":1773},"). If no plugin name has been given yet, ask the user before writing env var names.",{"type":61,"tag":545,"props":1775,"children":1777},{"className":593,"code":1776,"language":21,"meta":553,"style":553},"import asyncio\nimport os\nimport threading\nimport time\nimport airflow_client.client as airflow_sdk\nimport requests\n\nAIRFLOW_HOST  = os.environ.get(\"MYPLUGIN_HOST\",     \"http:\u002F\u002Flocalhost:8080\")\nAIRFLOW_USER  = os.environ.get(\"MYPLUGIN_USERNAME\", \"admin\")\nAIRFLOW_PASS  = os.environ.get(\"MYPLUGIN_PASSWORD\", \"admin\")\nAIRFLOW_TOKEN = os.environ.get(\"MYPLUGIN_TOKEN\")    # Astronomer Astro: Deployment API token\n\n_cached_token: str | None = None\n_token_expires_at: float  = 0.0\n_token_lock = threading.Lock()\n\n\ndef _fetch_fresh_token() -> str:\n    \"\"\"Exchange username\u002Fpassword for a JWT via Airflow's auth endpoint.\"\"\"\n    response = requests.post(\n        f\"{AIRFLOW_HOST}\u002Fauth\u002Ftoken\",\n        json={\"username\": AIRFLOW_USER, \"password\": AIRFLOW_PASS},\n        timeout=10,\n    )\n    response.raise_for_status()\n    return response.json()[\"access_token\"]\n\n\ndef _get_token() -> str:\n    # Astronomer Astro production: use static Deployment API token directly\n    if AIRFLOW_TOKEN:\n        return AIRFLOW_TOKEN\n    global _cached_token, _token_expires_at\n    now = time.monotonic()\n    # Fast path — no lock if still valid\n    if _cached_token and now \u003C _token_expires_at:\n        return _cached_token\n    # Slow path — one thread refreshes, others wait\n    with _token_lock:\n        if _cached_token and now \u003C _token_expires_at:\n            return _cached_token\n        _cached_token = _fetch_fresh_token()\n        _token_expires_at = now + 55 * 60  # refresh 5 min before 1-hour expiry\n    return _cached_token\n\n\ndef _make_config() -> airflow_sdk.Configuration:\n    config = airflow_sdk.Configuration(host=AIRFLOW_HOST)\n    config.access_token = _get_token()\n    return config\n",[1778],{"type":61,"tag":103,"props":1779,"children":1780},{"__ignoreMap":553},[1781,1789,1797,1805,1813,1821,1829,1836,1844,1852,1860,1868,1875,1883,1891,1899,1906,1913,1921,1929,1937,1945,1953,1961,1969,1977,1985,1992,1999,2007,2015,2023,2031,2039,2047,2055,2063,2072,2081,2090,2099,2108,2117,2126,2135,2143,2151,2160,2169,2178],{"type":61,"tag":600,"props":1782,"children":1783},{"class":602,"line":603},[1784],{"type":61,"tag":600,"props":1785,"children":1786},{},[1787],{"type":67,"value":1788},"import asyncio\n",{"type":61,"tag":600,"props":1790,"children":1791},{"class":602,"line":612},[1792],{"type":61,"tag":600,"props":1793,"children":1794},{},[1795],{"type":67,"value":1796},"import os\n",{"type":61,"tag":600,"props":1798,"children":1799},{"class":602,"line":621},[1800],{"type":61,"tag":600,"props":1801,"children":1802},{},[1803],{"type":67,"value":1804},"import threading\n",{"type":61,"tag":600,"props":1806,"children":1807},{"class":602,"line":630},[1808],{"type":61,"tag":600,"props":1809,"children":1810},{},[1811],{"type":67,"value":1812},"import time\n",{"type":61,"tag":600,"props":1814,"children":1815},{"class":602,"line":639},[1816],{"type":61,"tag":600,"props":1817,"children":1818},{},[1819],{"type":67,"value":1820},"import airflow_client.client as airflow_sdk\n",{"type":61,"tag":600,"props":1822,"children":1823},{"class":602,"line":648},[1824],{"type":61,"tag":600,"props":1825,"children":1826},{},[1827],{"type":67,"value":1828},"import requests\n",{"type":61,"tag":600,"props":1830,"children":1831},{"class":602,"line":658},[1832],{"type":61,"tag":600,"props":1833,"children":1834},{"emptyLinePlaceholder":652},[1835],{"type":67,"value":655},{"type":61,"tag":600,"props":1837,"children":1838},{"class":602,"line":667},[1839],{"type":61,"tag":600,"props":1840,"children":1841},{},[1842],{"type":67,"value":1843},"AIRFLOW_HOST  = os.environ.get(\"MYPLUGIN_HOST\",     \"http:\u002F\u002Flocalhost:8080\")\n",{"type":61,"tag":600,"props":1845,"children":1846},{"class":602,"line":675},[1847],{"type":61,"tag":600,"props":1848,"children":1849},{},[1850],{"type":67,"value":1851},"AIRFLOW_USER  = os.environ.get(\"MYPLUGIN_USERNAME\", \"admin\")\n",{"type":61,"tag":600,"props":1853,"children":1854},{"class":602,"line":684},[1855],{"type":61,"tag":600,"props":1856,"children":1857},{},[1858],{"type":67,"value":1859},"AIRFLOW_PASS  = os.environ.get(\"MYPLUGIN_PASSWORD\", \"admin\")\n",{"type":61,"tag":600,"props":1861,"children":1862},{"class":602,"line":692},[1863],{"type":61,"tag":600,"props":1864,"children":1865},{},[1866],{"type":67,"value":1867},"AIRFLOW_TOKEN = os.environ.get(\"MYPLUGIN_TOKEN\")    # Astronomer Astro: Deployment API token\n",{"type":61,"tag":600,"props":1869,"children":1870},{"class":602,"line":701},[1871],{"type":61,"tag":600,"props":1872,"children":1873},{"emptyLinePlaceholder":652},[1874],{"type":67,"value":655},{"type":61,"tag":600,"props":1876,"children":1877},{"class":602,"line":710},[1878],{"type":61,"tag":600,"props":1879,"children":1880},{},[1881],{"type":67,"value":1882},"_cached_token: str | None = None\n",{"type":61,"tag":600,"props":1884,"children":1885},{"class":602,"line":719},[1886],{"type":61,"tag":600,"props":1887,"children":1888},{},[1889],{"type":67,"value":1890},"_token_expires_at: float  = 0.0\n",{"type":61,"tag":600,"props":1892,"children":1893},{"class":602,"line":727},[1894],{"type":61,"tag":600,"props":1895,"children":1896},{},[1897],{"type":67,"value":1898},"_token_lock = threading.Lock()\n",{"type":61,"tag":600,"props":1900,"children":1901},{"class":602,"line":735},[1902],{"type":61,"tag":600,"props":1903,"children":1904},{"emptyLinePlaceholder":652},[1905],{"type":67,"value":655},{"type":61,"tag":600,"props":1907,"children":1908},{"class":602,"line":744},[1909],{"type":61,"tag":600,"props":1910,"children":1911},{"emptyLinePlaceholder":652},[1912],{"type":67,"value":655},{"type":61,"tag":600,"props":1914,"children":1915},{"class":602,"line":753},[1916],{"type":61,"tag":600,"props":1917,"children":1918},{},[1919],{"type":67,"value":1920},"def _fetch_fresh_token() -> str:\n",{"type":61,"tag":600,"props":1922,"children":1923},{"class":602,"line":761},[1924],{"type":61,"tag":600,"props":1925,"children":1926},{},[1927],{"type":67,"value":1928},"    \"\"\"Exchange username\u002Fpassword for a JWT via Airflow's auth endpoint.\"\"\"\n",{"type":61,"tag":600,"props":1930,"children":1931},{"class":602,"line":770},[1932],{"type":61,"tag":600,"props":1933,"children":1934},{},[1935],{"type":67,"value":1936},"    response = requests.post(\n",{"type":61,"tag":600,"props":1938,"children":1939},{"class":602,"line":779},[1940],{"type":61,"tag":600,"props":1941,"children":1942},{},[1943],{"type":67,"value":1944},"        f\"{AIRFLOW_HOST}\u002Fauth\u002Ftoken\",\n",{"type":61,"tag":600,"props":1946,"children":1947},{"class":602,"line":788},[1948],{"type":61,"tag":600,"props":1949,"children":1950},{},[1951],{"type":67,"value":1952},"        json={\"username\": AIRFLOW_USER, \"password\": AIRFLOW_PASS},\n",{"type":61,"tag":600,"props":1954,"children":1955},{"class":602,"line":797},[1956],{"type":61,"tag":600,"props":1957,"children":1958},{},[1959],{"type":67,"value":1960},"        timeout=10,\n",{"type":61,"tag":600,"props":1962,"children":1963},{"class":602,"line":806},[1964],{"type":61,"tag":600,"props":1965,"children":1966},{},[1967],{"type":67,"value":1968},"    )\n",{"type":61,"tag":600,"props":1970,"children":1971},{"class":602,"line":815},[1972],{"type":61,"tag":600,"props":1973,"children":1974},{},[1975],{"type":67,"value":1976},"    response.raise_for_status()\n",{"type":61,"tag":600,"props":1978,"children":1979},{"class":602,"line":824},[1980],{"type":61,"tag":600,"props":1981,"children":1982},{},[1983],{"type":67,"value":1984},"    return response.json()[\"access_token\"]\n",{"type":61,"tag":600,"props":1986,"children":1987},{"class":602,"line":832},[1988],{"type":61,"tag":600,"props":1989,"children":1990},{"emptyLinePlaceholder":652},[1991],{"type":67,"value":655},{"type":61,"tag":600,"props":1993,"children":1994},{"class":602,"line":841},[1995],{"type":61,"tag":600,"props":1996,"children":1997},{"emptyLinePlaceholder":652},[1998],{"type":67,"value":655},{"type":61,"tag":600,"props":2000,"children":2001},{"class":602,"line":849},[2002],{"type":61,"tag":600,"props":2003,"children":2004},{},[2005],{"type":67,"value":2006},"def _get_token() -> str:\n",{"type":61,"tag":600,"props":2008,"children":2009},{"class":602,"line":857},[2010],{"type":61,"tag":600,"props":2011,"children":2012},{},[2013],{"type":67,"value":2014},"    # Astronomer Astro production: use static Deployment API token directly\n",{"type":61,"tag":600,"props":2016,"children":2017},{"class":602,"line":866},[2018],{"type":61,"tag":600,"props":2019,"children":2020},{},[2021],{"type":67,"value":2022},"    if AIRFLOW_TOKEN:\n",{"type":61,"tag":600,"props":2024,"children":2025},{"class":602,"line":875},[2026],{"type":61,"tag":600,"props":2027,"children":2028},{},[2029],{"type":67,"value":2030},"        return AIRFLOW_TOKEN\n",{"type":61,"tag":600,"props":2032,"children":2033},{"class":602,"line":884},[2034],{"type":61,"tag":600,"props":2035,"children":2036},{},[2037],{"type":67,"value":2038},"    global _cached_token, _token_expires_at\n",{"type":61,"tag":600,"props":2040,"children":2041},{"class":602,"line":893},[2042],{"type":61,"tag":600,"props":2043,"children":2044},{},[2045],{"type":67,"value":2046},"    now = time.monotonic()\n",{"type":61,"tag":600,"props":2048,"children":2049},{"class":602,"line":902},[2050],{"type":61,"tag":600,"props":2051,"children":2052},{},[2053],{"type":67,"value":2054},"    # Fast path — no lock if still valid\n",{"type":61,"tag":600,"props":2056,"children":2057},{"class":602,"line":910},[2058],{"type":61,"tag":600,"props":2059,"children":2060},{},[2061],{"type":67,"value":2062},"    if _cached_token and now \u003C _token_expires_at:\n",{"type":61,"tag":600,"props":2064,"children":2066},{"class":602,"line":2065},37,[2067],{"type":61,"tag":600,"props":2068,"children":2069},{},[2070],{"type":67,"value":2071},"        return _cached_token\n",{"type":61,"tag":600,"props":2073,"children":2075},{"class":602,"line":2074},38,[2076],{"type":61,"tag":600,"props":2077,"children":2078},{},[2079],{"type":67,"value":2080},"    # Slow path — one thread refreshes, others wait\n",{"type":61,"tag":600,"props":2082,"children":2084},{"class":602,"line":2083},39,[2085],{"type":61,"tag":600,"props":2086,"children":2087},{},[2088],{"type":67,"value":2089},"    with _token_lock:\n",{"type":61,"tag":600,"props":2091,"children":2093},{"class":602,"line":2092},40,[2094],{"type":61,"tag":600,"props":2095,"children":2096},{},[2097],{"type":67,"value":2098},"        if _cached_token and now \u003C _token_expires_at:\n",{"type":61,"tag":600,"props":2100,"children":2102},{"class":602,"line":2101},41,[2103],{"type":61,"tag":600,"props":2104,"children":2105},{},[2106],{"type":67,"value":2107},"            return _cached_token\n",{"type":61,"tag":600,"props":2109,"children":2111},{"class":602,"line":2110},42,[2112],{"type":61,"tag":600,"props":2113,"children":2114},{},[2115],{"type":67,"value":2116},"        _cached_token = _fetch_fresh_token()\n",{"type":61,"tag":600,"props":2118,"children":2120},{"class":602,"line":2119},43,[2121],{"type":61,"tag":600,"props":2122,"children":2123},{},[2124],{"type":67,"value":2125},"        _token_expires_at = now + 55 * 60  # refresh 5 min before 1-hour expiry\n",{"type":61,"tag":600,"props":2127,"children":2129},{"class":602,"line":2128},44,[2130],{"type":61,"tag":600,"props":2131,"children":2132},{},[2133],{"type":67,"value":2134},"    return _cached_token\n",{"type":61,"tag":600,"props":2136,"children":2138},{"class":602,"line":2137},45,[2139],{"type":61,"tag":600,"props":2140,"children":2141},{"emptyLinePlaceholder":652},[2142],{"type":67,"value":655},{"type":61,"tag":600,"props":2144,"children":2146},{"class":602,"line":2145},46,[2147],{"type":61,"tag":600,"props":2148,"children":2149},{"emptyLinePlaceholder":652},[2150],{"type":67,"value":655},{"type":61,"tag":600,"props":2152,"children":2154},{"class":602,"line":2153},47,[2155],{"type":61,"tag":600,"props":2156,"children":2157},{},[2158],{"type":67,"value":2159},"def _make_config() -> airflow_sdk.Configuration:\n",{"type":61,"tag":600,"props":2161,"children":2163},{"class":602,"line":2162},48,[2164],{"type":61,"tag":600,"props":2165,"children":2166},{},[2167],{"type":67,"value":2168},"    config = airflow_sdk.Configuration(host=AIRFLOW_HOST)\n",{"type":61,"tag":600,"props":2170,"children":2172},{"class":602,"line":2171},49,[2173],{"type":61,"tag":600,"props":2174,"children":2175},{},[2176],{"type":67,"value":2177},"    config.access_token = _get_token()\n",{"type":61,"tag":600,"props":2179,"children":2181},{"class":602,"line":2180},50,[2182],{"type":61,"tag":600,"props":2183,"children":2184},{},[2185],{"type":67,"value":2186},"    return config\n",{"type":61,"tag":70,"props":2188,"children":2189},{},[2190],{"type":67,"value":2191},"After implementing auth, tell the user:",{"type":61,"tag":2193,"props":2194,"children":2195},"ul",{},[2196,2228],{"type":61,"tag":215,"props":2197,"children":2198},{},[2199,2204,2206,2212,2213,2219,2220,2226],{"type":61,"tag":83,"props":2200,"children":2201},{},[2202],{"type":67,"value":2203},"Local development",{"type":67,"value":2205},": set ",{"type":61,"tag":103,"props":2207,"children":2209},{"className":2208},[],[2210],{"type":67,"value":2211},"MYPLUGIN_USERNAME",{"type":67,"value":272},{"type":61,"tag":103,"props":2214,"children":2216},{"className":2215},[],[2217],{"type":67,"value":2218},"MYPLUGIN_PASSWORD",{"type":67,"value":566},{"type":61,"tag":103,"props":2221,"children":2223},{"className":2222},[],[2224],{"type":67,"value":2225},".env",{"type":67,"value":2227}," — JWT exchange happens automatically.",{"type":61,"tag":215,"props":2229,"children":2230},{},[2231,2236,2238,2244,2246,2286,2290,2295,2296,2301],{"type":61,"tag":83,"props":2232,"children":2233},{},[2234],{"type":67,"value":2235},"Astronomer Astro (production)",{"type":67,"value":2237},": create a Deployment API token and set it as ",{"type":61,"tag":103,"props":2239,"children":2241},{"className":2240},[],[2242],{"type":67,"value":2243},"MYPLUGIN_TOKEN",{"type":67,"value":2245}," — the JWT exchange is skipped entirely:",{"type":61,"tag":211,"props":2247,"children":2248},{},[2249,2272,2277],{"type":61,"tag":215,"props":2250,"children":2251},{},[2252,2254,2259,2261,2266,2267],{"type":67,"value":2253},"Astro UI → open the Deployment → ",{"type":61,"tag":83,"props":2255,"children":2256},{},[2257],{"type":67,"value":2258},"Access",{"type":67,"value":2260}," → ",{"type":61,"tag":83,"props":2262,"children":2263},{},[2264],{"type":67,"value":2265},"API Tokens",{"type":67,"value":2260},{"type":61,"tag":83,"props":2268,"children":2269},{},[2270],{"type":67,"value":2271},"+ Deployment API Token",{"type":61,"tag":215,"props":2273,"children":2274},{},[2275],{"type":67,"value":2276},"Copy the token value (shown only once)",{"type":61,"tag":215,"props":2278,"children":2279},{},[2280],{"type":61,"tag":103,"props":2281,"children":2283},{"className":2282},[],[2284],{"type":67,"value":2285},"astro deployment variable create MYPLUGIN_TOKEN=\u003Ctoken>",{"type":61,"tag":2287,"props":2288,"children":2289},"br",{},[],{"type":61,"tag":103,"props":2291,"children":2293},{"className":2292},[],[2294],{"type":67,"value":2211},{"type":67,"value":272},{"type":61,"tag":103,"props":2297,"children":2299},{"className":2298},[],[2300],{"type":67,"value":2218},{"type":67,"value":2302}," are not needed on Astro.",{"type":61,"tag":204,"props":2304,"children":2306},{"id":2305},"wrapping-sdk-calls-with-asyncioto_thread",[2307],{"type":67,"value":2308},"Wrapping SDK calls with asyncio.to_thread",{"type":61,"tag":545,"props":2310,"children":2312},{"className":593,"code":2311,"language":21,"meta":553,"style":553},"from fastapi import HTTPException\nfrom airflow_client.client.api import DAGApi\n\n@app.get(\"\u002Fapi\u002Fdags\")\nasync def list_dags():\n    try:\n        def _fetch():\n            with airflow_sdk.ApiClient(_make_config()) as client:\n                return DAGApi(client).get_dags(limit=100).dags\n        dags = await asyncio.to_thread(_fetch)\n        return [{\"dag_id\": d.dag_id, \"is_paused\": d.is_paused, \"timetable_summary\": d.timetable_summary} for d in dags]\n    except Exception as e:\n        raise HTTPException(status_code=500, detail=str(e))\n",[2313],{"type":61,"tag":103,"props":2314,"children":2315},{"__ignoreMap":553},[2316,2324,2332,2339,2347,2355,2363,2371,2379,2387,2395,2403,2411],{"type":61,"tag":600,"props":2317,"children":2318},{"class":602,"line":603},[2319],{"type":61,"tag":600,"props":2320,"children":2321},{},[2322],{"type":67,"value":2323},"from fastapi import HTTPException\n",{"type":61,"tag":600,"props":2325,"children":2326},{"class":602,"line":612},[2327],{"type":61,"tag":600,"props":2328,"children":2329},{},[2330],{"type":67,"value":2331},"from airflow_client.client.api import DAGApi\n",{"type":61,"tag":600,"props":2333,"children":2334},{"class":602,"line":621},[2335],{"type":61,"tag":600,"props":2336,"children":2337},{"emptyLinePlaceholder":652},[2338],{"type":67,"value":655},{"type":61,"tag":600,"props":2340,"children":2341},{"class":602,"line":630},[2342],{"type":61,"tag":600,"props":2343,"children":2344},{},[2345],{"type":67,"value":2346},"@app.get(\"\u002Fapi\u002Fdags\")\n",{"type":61,"tag":600,"props":2348,"children":2349},{"class":602,"line":639},[2350],{"type":61,"tag":600,"props":2351,"children":2352},{},[2353],{"type":67,"value":2354},"async def list_dags():\n",{"type":61,"tag":600,"props":2356,"children":2357},{"class":602,"line":648},[2358],{"type":61,"tag":600,"props":2359,"children":2360},{},[2361],{"type":67,"value":2362},"    try:\n",{"type":61,"tag":600,"props":2364,"children":2365},{"class":602,"line":658},[2366],{"type":61,"tag":600,"props":2367,"children":2368},{},[2369],{"type":67,"value":2370},"        def _fetch():\n",{"type":61,"tag":600,"props":2372,"children":2373},{"class":602,"line":667},[2374],{"type":61,"tag":600,"props":2375,"children":2376},{},[2377],{"type":67,"value":2378},"            with airflow_sdk.ApiClient(_make_config()) as client:\n",{"type":61,"tag":600,"props":2380,"children":2381},{"class":602,"line":675},[2382],{"type":61,"tag":600,"props":2383,"children":2384},{},[2385],{"type":67,"value":2386},"                return DAGApi(client).get_dags(limit=100).dags\n",{"type":61,"tag":600,"props":2388,"children":2389},{"class":602,"line":684},[2390],{"type":61,"tag":600,"props":2391,"children":2392},{},[2393],{"type":67,"value":2394},"        dags = await asyncio.to_thread(_fetch)\n",{"type":61,"tag":600,"props":2396,"children":2397},{"class":602,"line":692},[2398],{"type":61,"tag":600,"props":2399,"children":2400},{},[2401],{"type":67,"value":2402},"        return [{\"dag_id\": d.dag_id, \"is_paused\": d.is_paused, \"timetable_summary\": d.timetable_summary} for d in dags]\n",{"type":61,"tag":600,"props":2404,"children":2405},{"class":602,"line":701},[2406],{"type":61,"tag":600,"props":2407,"children":2408},{},[2409],{"type":67,"value":2410},"    except Exception as e:\n",{"type":61,"tag":600,"props":2412,"children":2413},{"class":602,"line":710},[2414],{"type":61,"tag":600,"props":2415,"children":2416},{},[2417],{"type":67,"value":2418},"        raise HTTPException(status_code=500, detail=str(e))\n",{"type":61,"tag":76,"props":2420,"children":2421},{},[2422],{"type":61,"tag":70,"props":2423,"children":2424},{},[2425,2430,2432,2441,2443,2449,2451,2457,2458,2464,2465,2471,2472,2478,2479,2485,2486,2492,2493,2499,2500,2506,2507,2513],{"type":61,"tag":83,"props":2426,"children":2427},{},[2428],{"type":67,"value":2429},"API field names",{"type":67,"value":2431},": Never guess response field names — verify against the ",{"type":61,"tag":2433,"props":2434,"children":2438},"a",{"href":2435,"rel":2436},"https:\u002F\u002Fairflow.apache.org\u002Fdocs\u002Fapache-airflow\u002Fstable\u002Fstable-rest-api-ref.html",[2437],"nofollow",[2439],{"type":67,"value":2440},"REST API reference",{"type":67,"value":2442},". Key ",{"type":61,"tag":103,"props":2444,"children":2446},{"className":2445},[],[2447],{"type":67,"value":2448},"DAGResponse",{"type":67,"value":2450}," fields: ",{"type":61,"tag":103,"props":2452,"children":2454},{"className":2453},[],[2455],{"type":67,"value":2456},"dag_id",{"type":67,"value":110},{"type":61,"tag":103,"props":2459,"children":2461},{"className":2460},[],[2462],{"type":67,"value":2463},"dag_display_name",{"type":67,"value":110},{"type":61,"tag":103,"props":2466,"children":2468},{"className":2467},[],[2469],{"type":67,"value":2470},"description",{"type":67,"value":110},{"type":61,"tag":103,"props":2473,"children":2475},{"className":2474},[],[2476],{"type":67,"value":2477},"is_paused",{"type":67,"value":110},{"type":61,"tag":103,"props":2480,"children":2482},{"className":2481},[],[2483],{"type":67,"value":2484},"timetable_summary",{"type":67,"value":110},{"type":61,"tag":103,"props":2487,"children":2489},{"className":2488},[],[2490],{"type":67,"value":2491},"timetable_description",{"type":67,"value":110},{"type":61,"tag":103,"props":2494,"children":2496},{"className":2495},[],[2497],{"type":67,"value":2498},"fileloc",{"type":67,"value":110},{"type":61,"tag":103,"props":2501,"children":2503},{"className":2502},[],[2504],{"type":67,"value":2505},"owners",{"type":67,"value":110},{"type":61,"tag":103,"props":2508,"children":2510},{"className":2509},[],[2511],{"type":67,"value":2512},"tags",{"type":67,"value":2514},".",{"type":61,"tag":70,"props":2516,"children":2517},{},[2518,2520,2526,2528,2534],{"type":67,"value":2519},"The pattern is always: define a plain inner ",{"type":61,"tag":103,"props":2521,"children":2523},{"className":2522},[],[2524],{"type":67,"value":2525},"def _fetch()",{"type":67,"value":2527}," with all SDK logic, then ",{"type":61,"tag":103,"props":2529,"children":2531},{"className":2530},[],[2532],{"type":67,"value":2533},"await asyncio.to_thread(_fetch)",{"type":67,"value":2514},{"type":61,"tag":204,"props":2536,"children":2538},{"id":2537},"alternative-direct-database-access",[2539],{"type":67,"value":2540},"Alternative: Direct database access",{"type":61,"tag":76,"props":2542,"children":2543},{},[2544],{"type":61,"tag":70,"props":2545,"children":2546},{},[2547,2552,2554,2559],{"type":61,"tag":83,"props":2548,"children":2549},{},[2550],{"type":67,"value":2551},"Warning — use with caution and tell the user.",{"type":67,"value":2553}," 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 ",{"type":61,"tag":103,"props":2555,"children":2557},{"className":2556},[],[2558],{"type":67,"value":1614},{"type":67,"value":2560}," unless the operation is not exposed via the REST API.\"",{"type":61,"tag":70,"props":2562,"children":2563},{},[2564,2566,2571,2573,2578],{"type":67,"value":2565},"Since FastAPI plugin endpoints run inside the ",{"type":61,"tag":83,"props":2567,"children":2568},{},[2569],{"type":67,"value":2570},"API server process",{"type":67,"value":2572}," (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 ",{"type":61,"tag":103,"props":2574,"children":2576},{"className":2575},[],[2577],{"type":67,"value":257},{"type":67,"value":2579}," — SQLAlchemy queries are blocking.",{"type":61,"tag":545,"props":2581,"children":2583},{"className":593,"code":2582,"language":21,"meta":553,"style":553},"from airflow.models import DagBag, DagModel\nfrom airflow.utils.db import provide_session\n\n@app.get(\"\u002Fapi\u002Fdags\u002Fstatus\")\nasync def dag_status():\n    def _fetch():\n        @provide_session\n        def _query(session=None):\n            dagbag = DagBag()\n            paused = sum(\n                1 for dag_id in dagbag.dags\n                if (m := session.query(DagModel).filter(DagModel.dag_id == dag_id).first())\n                and m.is_paused\n            )\n            return {\"total\": len(dagbag.dags), \"paused\": paused}\n        return _query()\n    return await asyncio.to_thread(_fetch)\n",[2584],{"type":61,"tag":103,"props":2585,"children":2586},{"__ignoreMap":553},[2587,2595,2603,2610,2618,2626,2634,2642,2650,2658,2666,2674,2682,2690,2698,2706,2714],{"type":61,"tag":600,"props":2588,"children":2589},{"class":602,"line":603},[2590],{"type":61,"tag":600,"props":2591,"children":2592},{},[2593],{"type":67,"value":2594},"from airflow.models import DagBag, DagModel\n",{"type":61,"tag":600,"props":2596,"children":2597},{"class":602,"line":612},[2598],{"type":61,"tag":600,"props":2599,"children":2600},{},[2601],{"type":67,"value":2602},"from airflow.utils.db import provide_session\n",{"type":61,"tag":600,"props":2604,"children":2605},{"class":602,"line":621},[2606],{"type":61,"tag":600,"props":2607,"children":2608},{"emptyLinePlaceholder":652},[2609],{"type":67,"value":655},{"type":61,"tag":600,"props":2611,"children":2612},{"class":602,"line":630},[2613],{"type":61,"tag":600,"props":2614,"children":2615},{},[2616],{"type":67,"value":2617},"@app.get(\"\u002Fapi\u002Fdags\u002Fstatus\")\n",{"type":61,"tag":600,"props":2619,"children":2620},{"class":602,"line":639},[2621],{"type":61,"tag":600,"props":2622,"children":2623},{},[2624],{"type":67,"value":2625},"async def dag_status():\n",{"type":61,"tag":600,"props":2627,"children":2628},{"class":602,"line":648},[2629],{"type":61,"tag":600,"props":2630,"children":2631},{},[2632],{"type":67,"value":2633},"    def _fetch():\n",{"type":61,"tag":600,"props":2635,"children":2636},{"class":602,"line":658},[2637],{"type":61,"tag":600,"props":2638,"children":2639},{},[2640],{"type":67,"value":2641},"        @provide_session\n",{"type":61,"tag":600,"props":2643,"children":2644},{"class":602,"line":667},[2645],{"type":61,"tag":600,"props":2646,"children":2647},{},[2648],{"type":67,"value":2649},"        def _query(session=None):\n",{"type":61,"tag":600,"props":2651,"children":2652},{"class":602,"line":675},[2653],{"type":61,"tag":600,"props":2654,"children":2655},{},[2656],{"type":67,"value":2657},"            dagbag = DagBag()\n",{"type":61,"tag":600,"props":2659,"children":2660},{"class":602,"line":684},[2661],{"type":61,"tag":600,"props":2662,"children":2663},{},[2664],{"type":67,"value":2665},"            paused = sum(\n",{"type":61,"tag":600,"props":2667,"children":2668},{"class":602,"line":692},[2669],{"type":61,"tag":600,"props":2670,"children":2671},{},[2672],{"type":67,"value":2673},"                1 for dag_id in dagbag.dags\n",{"type":61,"tag":600,"props":2675,"children":2676},{"class":602,"line":701},[2677],{"type":61,"tag":600,"props":2678,"children":2679},{},[2680],{"type":67,"value":2681},"                if (m := session.query(DagModel).filter(DagModel.dag_id == dag_id).first())\n",{"type":61,"tag":600,"props":2683,"children":2684},{"class":602,"line":710},[2685],{"type":61,"tag":600,"props":2686,"children":2687},{},[2688],{"type":67,"value":2689},"                and m.is_paused\n",{"type":61,"tag":600,"props":2691,"children":2692},{"class":602,"line":719},[2693],{"type":61,"tag":600,"props":2694,"children":2695},{},[2696],{"type":67,"value":2697},"            )\n",{"type":61,"tag":600,"props":2699,"children":2700},{"class":602,"line":727},[2701],{"type":61,"tag":600,"props":2702,"children":2703},{},[2704],{"type":67,"value":2705},"            return {\"total\": len(dagbag.dags), \"paused\": paused}\n",{"type":61,"tag":600,"props":2707,"children":2708},{"class":602,"line":735},[2709],{"type":61,"tag":600,"props":2710,"children":2711},{},[2712],{"type":67,"value":2713},"        return _query()\n",{"type":61,"tag":600,"props":2715,"children":2716},{"class":602,"line":744},[2717],{"type":61,"tag":600,"props":2718,"children":2719},{},[2720],{"type":67,"value":2721},"    return await asyncio.to_thread(_fetch)\n",{"type":61,"tag":287,"props":2723,"children":2724},{},[],{"type":61,"tag":291,"props":2726,"children":2728},{"id":2727},"step-5-common-api-endpoint-patterns",[2729],{"type":67,"value":2730},"Step 5: Common API endpoint patterns",{"type":61,"tag":76,"props":2732,"children":2733},{},[2734],{"type":61,"tag":70,"props":2735,"children":2736},{},[2737,2742,2744,2750,2752,2763],{"type":61,"tag":83,"props":2738,"children":2739},{},[2740],{"type":67,"value":2741},"If you need an SDK method or field not shown in the examples below",{"type":67,"value":2743},", verify it before generating code — do not guess. Either run ",{"type":61,"tag":103,"props":2745,"children":2747},{"className":2746},[],[2748],{"type":67,"value":2749},"python3 -c \"from airflow_client.client.api import \u003CClass>; print([m for m in dir(\u003CClass>) if not m.startswith('_')])\"",{"type":67,"value":2751}," in any environment where the SDK is installed, or search the ",{"type":61,"tag":2433,"props":2753,"children":2756},{"href":2754,"rel":2755},"https:\u002F\u002Fgithub.com\u002Fapache\u002Fairflow-client-python",[2437],[2757],{"type":61,"tag":103,"props":2758,"children":2760},{"className":2759},[],[2761],{"type":67,"value":2762},"apache\u002Fairflow-client-python",{"type":67,"value":2764}," repo for the class definition.",{"type":61,"tag":545,"props":2766,"children":2768},{"className":593,"code":2767,"language":21,"meta":553,"style":553},"from airflow_client.client.api import DAGApi, DagRunApi\nfrom airflow_client.client.models import TriggerDAGRunPostBody, DAGPatchBody\n\n\n@app.post(\"\u002Fapi\u002Fdags\u002F{dag_id}\u002Ftrigger\")\nasync def trigger_dag(dag_id: str):\n    def _run():\n        with airflow_sdk.ApiClient(_make_config()) as client:\n            return DagRunApi(client).trigger_dag_run(dag_id, TriggerDAGRunPostBody())\n    result = await asyncio.to_thread(_run)\n    return {\"run_id\": result.dag_run_id, \"state\": normalize_state(result.state)}\n\n\n@app.patch(\"\u002Fapi\u002Fdags\u002F{dag_id}\u002Fpause\")\nasync def toggle_pause(dag_id: str, is_paused: bool):\n    def _run():\n        with airflow_sdk.ApiClient(_make_config()) as client:\n            DAGApi(client).patch_dag(dag_id, DAGPatchBody(is_paused=is_paused))\n    await asyncio.to_thread(_run)\n    return {\"dag_id\": dag_id, \"is_paused\": is_paused}\n\n\n@app.delete(\"\u002Fapi\u002Fdags\u002F{dag_id}\")\nasync def delete_dag(dag_id: str):\n    def _run():\n        with airflow_sdk.ApiClient(_make_config()) as client:\n            DAGApi(client).delete_dag(dag_id)\n    await asyncio.to_thread(_run)\n    return {\"deleted\": dag_id}\n\n\ndef normalize_state(raw) -> str:\n    \"\"\"Convert SDK enum objects to plain strings before sending to the frontend.\"\"\"\n    if raw is None:\n        return \"never_run\"\n    return str(raw).lower()\n",[2769],{"type":61,"tag":103,"props":2770,"children":2771},{"__ignoreMap":553},[2772,2780,2788,2795,2802,2810,2818,2826,2834,2842,2850,2858,2865,2872,2880,2888,2895,2902,2910,2918,2926,2933,2940,2948,2956,2963,2970,2978,2985,2993,3000,3007,3015,3023,3031,3039],{"type":61,"tag":600,"props":2773,"children":2774},{"class":602,"line":603},[2775],{"type":61,"tag":600,"props":2776,"children":2777},{},[2778],{"type":67,"value":2779},"from airflow_client.client.api import DAGApi, DagRunApi\n",{"type":61,"tag":600,"props":2781,"children":2782},{"class":602,"line":612},[2783],{"type":61,"tag":600,"props":2784,"children":2785},{},[2786],{"type":67,"value":2787},"from airflow_client.client.models import TriggerDAGRunPostBody, DAGPatchBody\n",{"type":61,"tag":600,"props":2789,"children":2790},{"class":602,"line":621},[2791],{"type":61,"tag":600,"props":2792,"children":2793},{"emptyLinePlaceholder":652},[2794],{"type":67,"value":655},{"type":61,"tag":600,"props":2796,"children":2797},{"class":602,"line":630},[2798],{"type":61,"tag":600,"props":2799,"children":2800},{"emptyLinePlaceholder":652},[2801],{"type":67,"value":655},{"type":61,"tag":600,"props":2803,"children":2804},{"class":602,"line":639},[2805],{"type":61,"tag":600,"props":2806,"children":2807},{},[2808],{"type":67,"value":2809},"@app.post(\"\u002Fapi\u002Fdags\u002F{dag_id}\u002Ftrigger\")\n",{"type":61,"tag":600,"props":2811,"children":2812},{"class":602,"line":648},[2813],{"type":61,"tag":600,"props":2814,"children":2815},{},[2816],{"type":67,"value":2817},"async def trigger_dag(dag_id: str):\n",{"type":61,"tag":600,"props":2819,"children":2820},{"class":602,"line":658},[2821],{"type":61,"tag":600,"props":2822,"children":2823},{},[2824],{"type":67,"value":2825},"    def _run():\n",{"type":61,"tag":600,"props":2827,"children":2828},{"class":602,"line":667},[2829],{"type":61,"tag":600,"props":2830,"children":2831},{},[2832],{"type":67,"value":2833},"        with airflow_sdk.ApiClient(_make_config()) as client:\n",{"type":61,"tag":600,"props":2835,"children":2836},{"class":602,"line":675},[2837],{"type":61,"tag":600,"props":2838,"children":2839},{},[2840],{"type":67,"value":2841},"            return DagRunApi(client).trigger_dag_run(dag_id, TriggerDAGRunPostBody())\n",{"type":61,"tag":600,"props":2843,"children":2844},{"class":602,"line":684},[2845],{"type":61,"tag":600,"props":2846,"children":2847},{},[2848],{"type":67,"value":2849},"    result = await asyncio.to_thread(_run)\n",{"type":61,"tag":600,"props":2851,"children":2852},{"class":602,"line":692},[2853],{"type":61,"tag":600,"props":2854,"children":2855},{},[2856],{"type":67,"value":2857},"    return {\"run_id\": result.dag_run_id, \"state\": normalize_state(result.state)}\n",{"type":61,"tag":600,"props":2859,"children":2860},{"class":602,"line":701},[2861],{"type":61,"tag":600,"props":2862,"children":2863},{"emptyLinePlaceholder":652},[2864],{"type":67,"value":655},{"type":61,"tag":600,"props":2866,"children":2867},{"class":602,"line":710},[2868],{"type":61,"tag":600,"props":2869,"children":2870},{"emptyLinePlaceholder":652},[2871],{"type":67,"value":655},{"type":61,"tag":600,"props":2873,"children":2874},{"class":602,"line":719},[2875],{"type":61,"tag":600,"props":2876,"children":2877},{},[2878],{"type":67,"value":2879},"@app.patch(\"\u002Fapi\u002Fdags\u002F{dag_id}\u002Fpause\")\n",{"type":61,"tag":600,"props":2881,"children":2882},{"class":602,"line":727},[2883],{"type":61,"tag":600,"props":2884,"children":2885},{},[2886],{"type":67,"value":2887},"async def toggle_pause(dag_id: str, is_paused: bool):\n",{"type":61,"tag":600,"props":2889,"children":2890},{"class":602,"line":735},[2891],{"type":61,"tag":600,"props":2892,"children":2893},{},[2894],{"type":67,"value":2825},{"type":61,"tag":600,"props":2896,"children":2897},{"class":602,"line":744},[2898],{"type":61,"tag":600,"props":2899,"children":2900},{},[2901],{"type":67,"value":2833},{"type":61,"tag":600,"props":2903,"children":2904},{"class":602,"line":753},[2905],{"type":61,"tag":600,"props":2906,"children":2907},{},[2908],{"type":67,"value":2909},"            DAGApi(client).patch_dag(dag_id, DAGPatchBody(is_paused=is_paused))\n",{"type":61,"tag":600,"props":2911,"children":2912},{"class":602,"line":761},[2913],{"type":61,"tag":600,"props":2914,"children":2915},{},[2916],{"type":67,"value":2917},"    await asyncio.to_thread(_run)\n",{"type":61,"tag":600,"props":2919,"children":2920},{"class":602,"line":770},[2921],{"type":61,"tag":600,"props":2922,"children":2923},{},[2924],{"type":67,"value":2925},"    return {\"dag_id\": dag_id, \"is_paused\": is_paused}\n",{"type":61,"tag":600,"props":2927,"children":2928},{"class":602,"line":779},[2929],{"type":61,"tag":600,"props":2930,"children":2931},{"emptyLinePlaceholder":652},[2932],{"type":67,"value":655},{"type":61,"tag":600,"props":2934,"children":2935},{"class":602,"line":788},[2936],{"type":61,"tag":600,"props":2937,"children":2938},{"emptyLinePlaceholder":652},[2939],{"type":67,"value":655},{"type":61,"tag":600,"props":2941,"children":2942},{"class":602,"line":797},[2943],{"type":61,"tag":600,"props":2944,"children":2945},{},[2946],{"type":67,"value":2947},"@app.delete(\"\u002Fapi\u002Fdags\u002F{dag_id}\")\n",{"type":61,"tag":600,"props":2949,"children":2950},{"class":602,"line":806},[2951],{"type":61,"tag":600,"props":2952,"children":2953},{},[2954],{"type":67,"value":2955},"async def delete_dag(dag_id: str):\n",{"type":61,"tag":600,"props":2957,"children":2958},{"class":602,"line":815},[2959],{"type":61,"tag":600,"props":2960,"children":2961},{},[2962],{"type":67,"value":2825},{"type":61,"tag":600,"props":2964,"children":2965},{"class":602,"line":824},[2966],{"type":61,"tag":600,"props":2967,"children":2968},{},[2969],{"type":67,"value":2833},{"type":61,"tag":600,"props":2971,"children":2972},{"class":602,"line":832},[2973],{"type":61,"tag":600,"props":2974,"children":2975},{},[2976],{"type":67,"value":2977},"            DAGApi(client).delete_dag(dag_id)\n",{"type":61,"tag":600,"props":2979,"children":2980},{"class":602,"line":841},[2981],{"type":61,"tag":600,"props":2982,"children":2983},{},[2984],{"type":67,"value":2917},{"type":61,"tag":600,"props":2986,"children":2987},{"class":602,"line":849},[2988],{"type":61,"tag":600,"props":2989,"children":2990},{},[2991],{"type":67,"value":2992},"    return {\"deleted\": dag_id}\n",{"type":61,"tag":600,"props":2994,"children":2995},{"class":602,"line":857},[2996],{"type":61,"tag":600,"props":2997,"children":2998},{"emptyLinePlaceholder":652},[2999],{"type":67,"value":655},{"type":61,"tag":600,"props":3001,"children":3002},{"class":602,"line":866},[3003],{"type":61,"tag":600,"props":3004,"children":3005},{"emptyLinePlaceholder":652},[3006],{"type":67,"value":655},{"type":61,"tag":600,"props":3008,"children":3009},{"class":602,"line":875},[3010],{"type":61,"tag":600,"props":3011,"children":3012},{},[3013],{"type":67,"value":3014},"def normalize_state(raw) -> str:\n",{"type":61,"tag":600,"props":3016,"children":3017},{"class":602,"line":884},[3018],{"type":61,"tag":600,"props":3019,"children":3020},{},[3021],{"type":67,"value":3022},"    \"\"\"Convert SDK enum objects to plain strings before sending to the frontend.\"\"\"\n",{"type":61,"tag":600,"props":3024,"children":3025},{"class":602,"line":893},[3026],{"type":61,"tag":600,"props":3027,"children":3028},{},[3029],{"type":67,"value":3030},"    if raw is None:\n",{"type":61,"tag":600,"props":3032,"children":3033},{"class":602,"line":902},[3034],{"type":61,"tag":600,"props":3035,"children":3036},{},[3037],{"type":67,"value":3038},"        return \"never_run\"\n",{"type":61,"tag":600,"props":3040,"children":3041},{"class":602,"line":910},[3042],{"type":61,"tag":600,"props":3043,"children":3044},{},[3045],{"type":67,"value":3046},"    return str(raw).lower()\n",{"type":61,"tag":204,"props":3048,"children":3050},{"id":3049},"dag-runs-task-instances-and-logs",[3051],{"type":67,"value":3052},"DAG runs, task instances, and logs",{"type":61,"tag":70,"props":3054,"children":3055},{},[3056,3058,3063],{"type":67,"value":3057},"These are the most common calls beyond basic DAG CRUD. For anything not shown here, consult the ",{"type":61,"tag":2433,"props":3059,"children":3061},{"href":2435,"rel":3060},[2437],[3062],{"type":67,"value":2440},{"type":67,"value":3064}," for available endpoints and the matching Python SDK class\u002Fmethod names.",{"type":61,"tag":545,"props":3066,"children":3068},{"className":593,"code":3067,"language":21,"meta":553,"style":553},"from airflow_client.client.api import DagRunApi, TaskInstanceApi\n\n# Latest run for a DAG\n@app.get(\"\u002Fapi\u002Fdags\u002F{dag_id}\u002Fruns\u002Flatest\")\nasync def latest_run(dag_id: str):\n    def _fetch():\n        with airflow_sdk.ApiClient(_make_config()) as client:\n            runs = DagRunApi(client).get_dag_runs(dag_id, limit=1, order_by=\"-start_date\").dag_runs\n            return runs[0] if runs else None\n    run = await asyncio.to_thread(_fetch)\n    if not run:\n        return {\"state\": \"never_run\"}\n    return {\"run_id\": run.dag_run_id, \"state\": normalize_state(run.state)}\n\n\n# Task instances for a specific run\n@app.get(\"\u002Fapi\u002Fdags\u002F{dag_id}\u002Fruns\u002F{run_id}\u002Ftasks\")\nasync def task_instances(dag_id: str, run_id: str):\n    def _fetch():\n        with airflow_sdk.ApiClient(_make_config()) as client:\n            return TaskInstanceApi(client).get_task_instances(dag_id, run_id).task_instances\n    tasks = await asyncio.to_thread(_fetch)\n    return [{\"task_id\": t.task_id, \"state\": normalize_state(t.state)} for t in tasks]\n\n\n# Task log (try_number starts at 1)\n@app.get(\"\u002Fapi\u002Fdags\u002F{dag_id}\u002Fruns\u002F{run_id}\u002Ftasks\u002F{task_id}\u002Flogs\u002F{try_number}\")\nasync def task_log(dag_id: str, run_id: str, task_id: str, try_number: int):\n    def _fetch():\n        with airflow_sdk.ApiClient(_make_config()) as client:\n            return TaskInstanceApi(client).get_log(\n                dag_id, run_id, task_id, try_number, map_index=-1\n            )\n    result = await asyncio.to_thread(_fetch)\n    return {\"log\": result.content if hasattr(result, \"content\") else str(result)}\n",[3069],{"type":61,"tag":103,"props":3070,"children":3071},{"__ignoreMap":553},[3072,3080,3087,3095,3103,3111,3118,3125,3133,3141,3149,3157,3165,3173,3180,3187,3195,3203,3211,3218,3225,3233,3241,3249,3256,3263,3271,3279,3287,3294,3301,3309,3317,3324,3332],{"type":61,"tag":600,"props":3073,"children":3074},{"class":602,"line":603},[3075],{"type":61,"tag":600,"props":3076,"children":3077},{},[3078],{"type":67,"value":3079},"from airflow_client.client.api import DagRunApi, TaskInstanceApi\n",{"type":61,"tag":600,"props":3081,"children":3082},{"class":602,"line":612},[3083],{"type":61,"tag":600,"props":3084,"children":3085},{"emptyLinePlaceholder":652},[3086],{"type":67,"value":655},{"type":61,"tag":600,"props":3088,"children":3089},{"class":602,"line":621},[3090],{"type":61,"tag":600,"props":3091,"children":3092},{},[3093],{"type":67,"value":3094},"# Latest run for a DAG\n",{"type":61,"tag":600,"props":3096,"children":3097},{"class":602,"line":630},[3098],{"type":61,"tag":600,"props":3099,"children":3100},{},[3101],{"type":67,"value":3102},"@app.get(\"\u002Fapi\u002Fdags\u002F{dag_id}\u002Fruns\u002Flatest\")\n",{"type":61,"tag":600,"props":3104,"children":3105},{"class":602,"line":639},[3106],{"type":61,"tag":600,"props":3107,"children":3108},{},[3109],{"type":67,"value":3110},"async def latest_run(dag_id: str):\n",{"type":61,"tag":600,"props":3112,"children":3113},{"class":602,"line":648},[3114],{"type":61,"tag":600,"props":3115,"children":3116},{},[3117],{"type":67,"value":2633},{"type":61,"tag":600,"props":3119,"children":3120},{"class":602,"line":658},[3121],{"type":61,"tag":600,"props":3122,"children":3123},{},[3124],{"type":67,"value":2833},{"type":61,"tag":600,"props":3126,"children":3127},{"class":602,"line":667},[3128],{"type":61,"tag":600,"props":3129,"children":3130},{},[3131],{"type":67,"value":3132},"            runs = DagRunApi(client).get_dag_runs(dag_id, limit=1, order_by=\"-start_date\").dag_runs\n",{"type":61,"tag":600,"props":3134,"children":3135},{"class":602,"line":675},[3136],{"type":61,"tag":600,"props":3137,"children":3138},{},[3139],{"type":67,"value":3140},"            return runs[0] if runs else None\n",{"type":61,"tag":600,"props":3142,"children":3143},{"class":602,"line":684},[3144],{"type":61,"tag":600,"props":3145,"children":3146},{},[3147],{"type":67,"value":3148},"    run = await asyncio.to_thread(_fetch)\n",{"type":61,"tag":600,"props":3150,"children":3151},{"class":602,"line":692},[3152],{"type":61,"tag":600,"props":3153,"children":3154},{},[3155],{"type":67,"value":3156},"    if not run:\n",{"type":61,"tag":600,"props":3158,"children":3159},{"class":602,"line":701},[3160],{"type":61,"tag":600,"props":3161,"children":3162},{},[3163],{"type":67,"value":3164},"        return {\"state\": \"never_run\"}\n",{"type":61,"tag":600,"props":3166,"children":3167},{"class":602,"line":710},[3168],{"type":61,"tag":600,"props":3169,"children":3170},{},[3171],{"type":67,"value":3172},"    return {\"run_id\": run.dag_run_id, \"state\": normalize_state(run.state)}\n",{"type":61,"tag":600,"props":3174,"children":3175},{"class":602,"line":719},[3176],{"type":61,"tag":600,"props":3177,"children":3178},{"emptyLinePlaceholder":652},[3179],{"type":67,"value":655},{"type":61,"tag":600,"props":3181,"children":3182},{"class":602,"line":727},[3183],{"type":61,"tag":600,"props":3184,"children":3185},{"emptyLinePlaceholder":652},[3186],{"type":67,"value":655},{"type":61,"tag":600,"props":3188,"children":3189},{"class":602,"line":735},[3190],{"type":61,"tag":600,"props":3191,"children":3192},{},[3193],{"type":67,"value":3194},"# Task instances for a specific run\n",{"type":61,"tag":600,"props":3196,"children":3197},{"class":602,"line":744},[3198],{"type":61,"tag":600,"props":3199,"children":3200},{},[3201],{"type":67,"value":3202},"@app.get(\"\u002Fapi\u002Fdags\u002F{dag_id}\u002Fruns\u002F{run_id}\u002Ftasks\")\n",{"type":61,"tag":600,"props":3204,"children":3205},{"class":602,"line":753},[3206],{"type":61,"tag":600,"props":3207,"children":3208},{},[3209],{"type":67,"value":3210},"async def task_instances(dag_id: str, run_id: str):\n",{"type":61,"tag":600,"props":3212,"children":3213},{"class":602,"line":761},[3214],{"type":61,"tag":600,"props":3215,"children":3216},{},[3217],{"type":67,"value":2633},{"type":61,"tag":600,"props":3219,"children":3220},{"class":602,"line":770},[3221],{"type":61,"tag":600,"props":3222,"children":3223},{},[3224],{"type":67,"value":2833},{"type":61,"tag":600,"props":3226,"children":3227},{"class":602,"line":779},[3228],{"type":61,"tag":600,"props":3229,"children":3230},{},[3231],{"type":67,"value":3232},"            return TaskInstanceApi(client).get_task_instances(dag_id, run_id).task_instances\n",{"type":61,"tag":600,"props":3234,"children":3235},{"class":602,"line":788},[3236],{"type":61,"tag":600,"props":3237,"children":3238},{},[3239],{"type":67,"value":3240},"    tasks = await asyncio.to_thread(_fetch)\n",{"type":61,"tag":600,"props":3242,"children":3243},{"class":602,"line":797},[3244],{"type":61,"tag":600,"props":3245,"children":3246},{},[3247],{"type":67,"value":3248},"    return [{\"task_id\": t.task_id, \"state\": normalize_state(t.state)} for t in tasks]\n",{"type":61,"tag":600,"props":3250,"children":3251},{"class":602,"line":806},[3252],{"type":61,"tag":600,"props":3253,"children":3254},{"emptyLinePlaceholder":652},[3255],{"type":67,"value":655},{"type":61,"tag":600,"props":3257,"children":3258},{"class":602,"line":815},[3259],{"type":61,"tag":600,"props":3260,"children":3261},{"emptyLinePlaceholder":652},[3262],{"type":67,"value":655},{"type":61,"tag":600,"props":3264,"children":3265},{"class":602,"line":824},[3266],{"type":61,"tag":600,"props":3267,"children":3268},{},[3269],{"type":67,"value":3270},"# Task log (try_number starts at 1)\n",{"type":61,"tag":600,"props":3272,"children":3273},{"class":602,"line":832},[3274],{"type":61,"tag":600,"props":3275,"children":3276},{},[3277],{"type":67,"value":3278},"@app.get(\"\u002Fapi\u002Fdags\u002F{dag_id}\u002Fruns\u002F{run_id}\u002Ftasks\u002F{task_id}\u002Flogs\u002F{try_number}\")\n",{"type":61,"tag":600,"props":3280,"children":3281},{"class":602,"line":841},[3282],{"type":61,"tag":600,"props":3283,"children":3284},{},[3285],{"type":67,"value":3286},"async def task_log(dag_id: str, run_id: str, task_id: str, try_number: int):\n",{"type":61,"tag":600,"props":3288,"children":3289},{"class":602,"line":849},[3290],{"type":61,"tag":600,"props":3291,"children":3292},{},[3293],{"type":67,"value":2633},{"type":61,"tag":600,"props":3295,"children":3296},{"class":602,"line":857},[3297],{"type":61,"tag":600,"props":3298,"children":3299},{},[3300],{"type":67,"value":2833},{"type":61,"tag":600,"props":3302,"children":3303},{"class":602,"line":866},[3304],{"type":61,"tag":600,"props":3305,"children":3306},{},[3307],{"type":67,"value":3308},"            return TaskInstanceApi(client).get_log(\n",{"type":61,"tag":600,"props":3310,"children":3311},{"class":602,"line":875},[3312],{"type":61,"tag":600,"props":3313,"children":3314},{},[3315],{"type":67,"value":3316},"                dag_id, run_id, task_id, try_number, map_index=-1\n",{"type":61,"tag":600,"props":3318,"children":3319},{"class":602,"line":884},[3320],{"type":61,"tag":600,"props":3321,"children":3322},{},[3323],{"type":67,"value":2697},{"type":61,"tag":600,"props":3325,"children":3326},{"class":602,"line":893},[3327],{"type":61,"tag":600,"props":3328,"children":3329},{},[3330],{"type":67,"value":3331},"    result = await asyncio.to_thread(_fetch)\n",{"type":61,"tag":600,"props":3333,"children":3334},{"class":602,"line":902},[3335],{"type":61,"tag":600,"props":3336,"children":3337},{},[3338],{"type":67,"value":3339},"    return {\"log\": result.content if hasattr(result, \"content\") else str(result)}\n",{"type":61,"tag":204,"props":3341,"children":3343},{"id":3342},"streaming-proxy",[3344],{"type":67,"value":3345},"Streaming proxy",{"type":61,"tag":70,"props":3347,"children":3348},{},[3349,3350,3356],{"type":67,"value":1716},{"type":61,"tag":103,"props":3351,"children":3353},{"className":3352},[],[3354],{"type":67,"value":3355},"StreamingResponse",{"type":67,"value":3357}," to proxy binary content from an external URL through the plugin — useful when the browser can't fetch the resource directly (CORS, auth, etc.):",{"type":61,"tag":545,"props":3359,"children":3361},{"className":593,"code":3360,"language":21,"meta":553,"style":553},"import requests\nfrom starlette.responses import StreamingResponse\n\n@app.get(\"\u002Fapi\u002Ffiles\u002F{filename}\")\nasync def proxy_file(filename: str):\n    def _stream():\n        r = requests.get(f\"https:\u002F\u002Ffiles.example.com\u002F{filename}\", stream=True)\n        r.raise_for_status()\n        return r\n    response = await asyncio.to_thread(_stream)\n    return StreamingResponse(\n        response.iter_content(chunk_size=8192),\n        media_type=\"application\u002Foctet-stream\",\n        headers={\"Content-Disposition\": f'attachment; filename=\"{filename}\"'},\n    )\n",[3362],{"type":61,"tag":103,"props":3363,"children":3364},{"__ignoreMap":553},[3365,3372,3380,3387,3395,3403,3411,3419,3427,3435,3443,3451,3459,3467,3475],{"type":61,"tag":600,"props":3366,"children":3367},{"class":602,"line":603},[3368],{"type":61,"tag":600,"props":3369,"children":3370},{},[3371],{"type":67,"value":1828},{"type":61,"tag":600,"props":3373,"children":3374},{"class":602,"line":612},[3375],{"type":61,"tag":600,"props":3376,"children":3377},{},[3378],{"type":67,"value":3379},"from starlette.responses import StreamingResponse\n",{"type":61,"tag":600,"props":3381,"children":3382},{"class":602,"line":621},[3383],{"type":61,"tag":600,"props":3384,"children":3385},{"emptyLinePlaceholder":652},[3386],{"type":67,"value":655},{"type":61,"tag":600,"props":3388,"children":3389},{"class":602,"line":630},[3390],{"type":61,"tag":600,"props":3391,"children":3392},{},[3393],{"type":67,"value":3394},"@app.get(\"\u002Fapi\u002Ffiles\u002F{filename}\")\n",{"type":61,"tag":600,"props":3396,"children":3397},{"class":602,"line":639},[3398],{"type":61,"tag":600,"props":3399,"children":3400},{},[3401],{"type":67,"value":3402},"async def proxy_file(filename: str):\n",{"type":61,"tag":600,"props":3404,"children":3405},{"class":602,"line":648},[3406],{"type":61,"tag":600,"props":3407,"children":3408},{},[3409],{"type":67,"value":3410},"    def _stream():\n",{"type":61,"tag":600,"props":3412,"children":3413},{"class":602,"line":658},[3414],{"type":61,"tag":600,"props":3415,"children":3416},{},[3417],{"type":67,"value":3418},"        r = requests.get(f\"https:\u002F\u002Ffiles.example.com\u002F{filename}\", stream=True)\n",{"type":61,"tag":600,"props":3420,"children":3421},{"class":602,"line":667},[3422],{"type":61,"tag":600,"props":3423,"children":3424},{},[3425],{"type":67,"value":3426},"        r.raise_for_status()\n",{"type":61,"tag":600,"props":3428,"children":3429},{"class":602,"line":675},[3430],{"type":61,"tag":600,"props":3431,"children":3432},{},[3433],{"type":67,"value":3434},"        return r\n",{"type":61,"tag":600,"props":3436,"children":3437},{"class":602,"line":684},[3438],{"type":61,"tag":600,"props":3439,"children":3440},{},[3441],{"type":67,"value":3442},"    response = await asyncio.to_thread(_stream)\n",{"type":61,"tag":600,"props":3444,"children":3445},{"class":602,"line":692},[3446],{"type":61,"tag":600,"props":3447,"children":3448},{},[3449],{"type":67,"value":3450},"    return StreamingResponse(\n",{"type":61,"tag":600,"props":3452,"children":3453},{"class":602,"line":701},[3454],{"type":61,"tag":600,"props":3455,"children":3456},{},[3457],{"type":67,"value":3458},"        response.iter_content(chunk_size=8192),\n",{"type":61,"tag":600,"props":3460,"children":3461},{"class":602,"line":710},[3462],{"type":61,"tag":600,"props":3463,"children":3464},{},[3465],{"type":67,"value":3466},"        media_type=\"application\u002Foctet-stream\",\n",{"type":61,"tag":600,"props":3468,"children":3469},{"class":602,"line":719},[3470],{"type":61,"tag":600,"props":3471,"children":3472},{},[3473],{"type":67,"value":3474},"        headers={\"Content-Disposition\": f'attachment; filename=\"{filename}\"'},\n",{"type":61,"tag":600,"props":3476,"children":3477},{"class":602,"line":727},[3478],{"type":61,"tag":600,"props":3479,"children":3480},{},[3481],{"type":67,"value":1968},{"type":61,"tag":70,"props":3483,"children":3484},{},[3485,3487,3493,3495,3501],{"type":67,"value":3486},"Note that ",{"type":61,"tag":103,"props":3488,"children":3490},{"className":3489},[],[3491],{"type":67,"value":3492},"requests.get()",{"type":67,"value":3494}," is blocking — fetch in ",{"type":61,"tag":103,"props":3496,"children":3498},{"className":3497},[],[3499],{"type":67,"value":3500},"asyncio.to_thread",{"type":67,"value":3502}," so the event loop isn't stalled while waiting for the remote server.",{"type":61,"tag":287,"props":3504,"children":3505},{},[],{"type":61,"tag":291,"props":3507,"children":3509},{"id":3508},"step-6-other-plugin-component-types",[3510],{"type":67,"value":3511},"Step 6: Other plugin component types",{"type":61,"tag":204,"props":3513,"children":3514},{"id":442},[3515],{"type":67,"value":3516},"Macros",{"type":61,"tag":70,"props":3518,"children":3519},{},[3520,3522,3527],{"type":67,"value":3521},"Macros are loaded by the ",{"type":61,"tag":83,"props":3523,"children":3524},{},[3525],{"type":67,"value":3526},"scheduler",{"type":67,"value":3528}," (and DAG processor), not the API server. Restart the scheduler after changes.",{"type":61,"tag":545,"props":3530,"children":3532},{"className":593,"code":3531,"language":21,"meta":553,"style":553},"from airflow.plugins_manager import AirflowPlugin\n\ndef format_confidence(confidence: float) -> str:\n    return f\"{confidence * 100:.2f}%\"\n\nclass MyPlugin(AirflowPlugin):\n    name = \"my_plugin\"\n    macros = [format_confidence]\n",[3533],{"type":61,"tag":103,"props":3534,"children":3535},{"__ignoreMap":553},[3536,3543,3550,3558,3566,3573,3580,3587],{"type":61,"tag":600,"props":3537,"children":3538},{"class":602,"line":603},[3539],{"type":61,"tag":600,"props":3540,"children":3541},{},[3542],{"type":67,"value":618},{"type":61,"tag":600,"props":3544,"children":3545},{"class":602,"line":612},[3546],{"type":61,"tag":600,"props":3547,"children":3548},{"emptyLinePlaceholder":652},[3549],{"type":67,"value":655},{"type":61,"tag":600,"props":3551,"children":3552},{"class":602,"line":621},[3553],{"type":61,"tag":600,"props":3554,"children":3555},{},[3556],{"type":67,"value":3557},"def format_confidence(confidence: float) -> str:\n",{"type":61,"tag":600,"props":3559,"children":3560},{"class":602,"line":630},[3561],{"type":61,"tag":600,"props":3562,"children":3563},{},[3564],{"type":67,"value":3565},"    return f\"{confidence * 100:.2f}%\"\n",{"type":61,"tag":600,"props":3567,"children":3568},{"class":602,"line":639},[3569],{"type":61,"tag":600,"props":3570,"children":3571},{"emptyLinePlaceholder":652},[3572],{"type":67,"value":655},{"type":61,"tag":600,"props":3574,"children":3575},{"class":602,"line":648},[3576],{"type":61,"tag":600,"props":3577,"children":3578},{},[3579],{"type":67,"value":741},{"type":61,"tag":600,"props":3581,"children":3582},{"class":602,"line":658},[3583],{"type":61,"tag":600,"props":3584,"children":3585},{},[3586],{"type":67,"value":750},{"type":61,"tag":600,"props":3588,"children":3589},{"class":602,"line":667},[3590],{"type":61,"tag":600,"props":3591,"children":3592},{},[3593],{"type":67,"value":3594},"    macros = [format_confidence]\n",{"type":61,"tag":70,"props":3596,"children":3597},{},[3598],{"type":67,"value":3599},"Use in any templated field — including with XCom:",{"type":61,"tag":545,"props":3601,"children":3604},{"className":3602,"code":3603,"language":67},[548],"{{ macros.my_plugin.format_confidence(0.95) }}\n\n{{ macros.my_plugin.format_confidence(ti.xcom_pull(task_ids='score_task')['confidence']) }}\n",[3605],{"type":61,"tag":103,"props":3606,"children":3607},{"__ignoreMap":553},[3608],{"type":67,"value":3603},{"type":61,"tag":70,"props":3610,"children":3611},{},[3612,3614,3620],{"type":67,"value":3613},"The naming pattern is always ",{"type":61,"tag":103,"props":3615,"children":3617},{"className":3616},[],[3618],{"type":67,"value":3619},"macros.{plugin_name}.{function_name}",{"type":67,"value":2514},{"type":61,"tag":204,"props":3622,"children":3624},{"id":3623},"middleware",[3625],{"type":67,"value":3626},"Middleware",{"type":61,"tag":70,"props":3628,"children":3629},{},[3630,3632,3637],{"type":67,"value":3631},"Middleware applies to ",{"type":61,"tag":83,"props":3633,"children":3634},{},[3635],{"type":67,"value":3636},"all",{"type":67,"value":3638}," Airflow API requests, including the built-in REST API and any FastAPI plugins. Use sparingly and filter requests explicitly if needed:",{"type":61,"tag":545,"props":3640,"children":3642},{"className":593,"code":3641,"language":21,"meta":553,"style":553},"from starlette.middleware.base import BaseHTTPMiddleware\nfrom fastapi import Request, Response\n\nclass AuditMiddleware(BaseHTTPMiddleware):\n    async def dispatch(self, request: Request, call_next) -> Response:\n        # runs before every request to the Airflow API server\n        response = await call_next(request)\n        return response\n\nclass MyPlugin(AirflowPlugin):\n    name = \"my_plugin\"\n    fastapi_root_middlewares = [\n        {\"middleware\": AuditMiddleware, \"args\": [], \"kwargs\": {}, \"name\": \"Audit\"}\n    ]\n",[3643],{"type":61,"tag":103,"props":3644,"children":3645},{"__ignoreMap":553},[3646,3654,3662,3669,3677,3685,3693,3701,3709,3716,3723,3730,3738,3746],{"type":61,"tag":600,"props":3647,"children":3648},{"class":602,"line":603},[3649],{"type":61,"tag":600,"props":3650,"children":3651},{},[3652],{"type":67,"value":3653},"from starlette.middleware.base import BaseHTTPMiddleware\n",{"type":61,"tag":600,"props":3655,"children":3656},{"class":602,"line":612},[3657],{"type":61,"tag":600,"props":3658,"children":3659},{},[3660],{"type":67,"value":3661},"from fastapi import Request, Response\n",{"type":61,"tag":600,"props":3663,"children":3664},{"class":602,"line":621},[3665],{"type":61,"tag":600,"props":3666,"children":3667},{"emptyLinePlaceholder":652},[3668],{"type":67,"value":655},{"type":61,"tag":600,"props":3670,"children":3671},{"class":602,"line":630},[3672],{"type":61,"tag":600,"props":3673,"children":3674},{},[3675],{"type":67,"value":3676},"class AuditMiddleware(BaseHTTPMiddleware):\n",{"type":61,"tag":600,"props":3678,"children":3679},{"class":602,"line":639},[3680],{"type":61,"tag":600,"props":3681,"children":3682},{},[3683],{"type":67,"value":3684},"    async def dispatch(self, request: Request, call_next) -> Response:\n",{"type":61,"tag":600,"props":3686,"children":3687},{"class":602,"line":648},[3688],{"type":61,"tag":600,"props":3689,"children":3690},{},[3691],{"type":67,"value":3692},"        # runs before every request to the Airflow API server\n",{"type":61,"tag":600,"props":3694,"children":3695},{"class":602,"line":658},[3696],{"type":61,"tag":600,"props":3697,"children":3698},{},[3699],{"type":67,"value":3700},"        response = await call_next(request)\n",{"type":61,"tag":600,"props":3702,"children":3703},{"class":602,"line":667},[3704],{"type":61,"tag":600,"props":3705,"children":3706},{},[3707],{"type":67,"value":3708},"        return response\n",{"type":61,"tag":600,"props":3710,"children":3711},{"class":602,"line":675},[3712],{"type":61,"tag":600,"props":3713,"children":3714},{"emptyLinePlaceholder":652},[3715],{"type":67,"value":655},{"type":61,"tag":600,"props":3717,"children":3718},{"class":602,"line":684},[3719],{"type":61,"tag":600,"props":3720,"children":3721},{},[3722],{"type":67,"value":741},{"type":61,"tag":600,"props":3724,"children":3725},{"class":602,"line":692},[3726],{"type":61,"tag":600,"props":3727,"children":3728},{},[3729],{"type":67,"value":750},{"type":61,"tag":600,"props":3731,"children":3732},{"class":602,"line":701},[3733],{"type":61,"tag":600,"props":3734,"children":3735},{},[3736],{"type":67,"value":3737},"    fastapi_root_middlewares = [\n",{"type":61,"tag":600,"props":3739,"children":3740},{"class":602,"line":710},[3741],{"type":61,"tag":600,"props":3742,"children":3743},{},[3744],{"type":67,"value":3745},"        {\"middleware\": AuditMiddleware, \"args\": [], \"kwargs\": {}, \"name\": \"Audit\"}\n",{"type":61,"tag":600,"props":3747,"children":3748},{"class":602,"line":719},[3749],{"type":61,"tag":600,"props":3750,"children":3751},{},[3752],{"type":67,"value":821},{"type":61,"tag":204,"props":3754,"children":3756},{"id":3755},"operator-extra-links",[3757],{"type":67,"value":3758},"Operator extra links",{"type":61,"tag":545,"props":3760,"children":3762},{"className":593,"code":3761,"language":21,"meta":553,"style":553},"from airflow.sdk.bases.operatorlink import BaseOperatorLink\n\nclass MyDashboardLink(BaseOperatorLink):\n    name = \"Open in Dashboard\"\n\n    def get_link(self, operator, *, ti_key, **context) -> str:\n        return f\"https:\u002F\u002Fmy-dashboard.example.com\u002Ftasks\u002F{ti_key.task_id}\"\n\nclass MyPlugin(AirflowPlugin):\n    name = \"my_plugin\"\n    global_operator_extra_links = [MyDashboardLink()]  # appears on every task\n    # operator_extra_links = [MyDashboardLink()]       # attach to specific operator instead\n",[3763],{"type":61,"tag":103,"props":3764,"children":3765},{"__ignoreMap":553},[3766,3774,3781,3789,3797,3804,3812,3820,3827,3834,3841,3849],{"type":61,"tag":600,"props":3767,"children":3768},{"class":602,"line":603},[3769],{"type":61,"tag":600,"props":3770,"children":3771},{},[3772],{"type":67,"value":3773},"from airflow.sdk.bases.operatorlink import BaseOperatorLink\n",{"type":61,"tag":600,"props":3775,"children":3776},{"class":602,"line":612},[3777],{"type":61,"tag":600,"props":3778,"children":3779},{"emptyLinePlaceholder":652},[3780],{"type":67,"value":655},{"type":61,"tag":600,"props":3782,"children":3783},{"class":602,"line":621},[3784],{"type":61,"tag":600,"props":3785,"children":3786},{},[3787],{"type":67,"value":3788},"class MyDashboardLink(BaseOperatorLink):\n",{"type":61,"tag":600,"props":3790,"children":3791},{"class":602,"line":630},[3792],{"type":61,"tag":600,"props":3793,"children":3794},{},[3795],{"type":67,"value":3796},"    name = \"Open in Dashboard\"\n",{"type":61,"tag":600,"props":3798,"children":3799},{"class":602,"line":639},[3800],{"type":61,"tag":600,"props":3801,"children":3802},{"emptyLinePlaceholder":652},[3803],{"type":67,"value":655},{"type":61,"tag":600,"props":3805,"children":3806},{"class":602,"line":648},[3807],{"type":61,"tag":600,"props":3808,"children":3809},{},[3810],{"type":67,"value":3811},"    def get_link(self, operator, *, ti_key, **context) -> str:\n",{"type":61,"tag":600,"props":3813,"children":3814},{"class":602,"line":658},[3815],{"type":61,"tag":600,"props":3816,"children":3817},{},[3818],{"type":67,"value":3819},"        return f\"https:\u002F\u002Fmy-dashboard.example.com\u002Ftasks\u002F{ti_key.task_id}\"\n",{"type":61,"tag":600,"props":3821,"children":3822},{"class":602,"line":667},[3823],{"type":61,"tag":600,"props":3824,"children":3825},{"emptyLinePlaceholder":652},[3826],{"type":67,"value":655},{"type":61,"tag":600,"props":3828,"children":3829},{"class":602,"line":675},[3830],{"type":61,"tag":600,"props":3831,"children":3832},{},[3833],{"type":67,"value":741},{"type":61,"tag":600,"props":3835,"children":3836},{"class":602,"line":684},[3837],{"type":61,"tag":600,"props":3838,"children":3839},{},[3840],{"type":67,"value":750},{"type":61,"tag":600,"props":3842,"children":3843},{"class":602,"line":692},[3844],{"type":61,"tag":600,"props":3845,"children":3846},{},[3847],{"type":67,"value":3848},"    global_operator_extra_links = [MyDashboardLink()]  # appears on every task\n",{"type":61,"tag":600,"props":3850,"children":3851},{"class":602,"line":701},[3852],{"type":61,"tag":600,"props":3853,"children":3854},{},[3855],{"type":67,"value":3856},"    # operator_extra_links = [MyDashboardLink()]       # attach to specific operator instead\n",{"type":61,"tag":204,"props":3858,"children":3860},{"id":3859},"react-apps",[3861],{"type":67,"value":3862},"React apps",{"type":61,"tag":70,"props":3864,"children":3865},{},[3866],{"type":67,"value":3867},"React apps are embedded as JavaScript bundles served via FastAPI. The bundle must expose itself as a global variable matching the plugin name:",{"type":61,"tag":545,"props":3869,"children":3871},{"className":1489,"code":3870,"language":1491,"meta":553,"style":553},"\u002F\u002F In your bundle (e.g. my-app.js)\nglobalThis['My Plugin'] = MyComponent;   \u002F\u002F matches plugin name\nglobalThis.AirflowPlugin = MyComponent;  \u002F\u002F fallback Airflow looks for\n",[3872],{"type":61,"tag":103,"props":3873,"children":3874},{"__ignoreMap":553},[3875,3883,3928],{"type":61,"tag":600,"props":3876,"children":3877},{"class":602,"line":603},[3878],{"type":61,"tag":600,"props":3879,"children":3880},{"style":1303},[3881],{"type":67,"value":3882},"\u002F\u002F In your bundle (e.g. my-app.js)\n",{"type":61,"tag":600,"props":3884,"children":3885},{"class":602,"line":612},[3886,3891,3895,3900,3904,3909,3913,3918,3923],{"type":61,"tag":600,"props":3887,"children":3888},{"style":1507},[3889],{"type":67,"value":3890},"globalThis[",{"type":61,"tag":600,"props":3892,"children":3893},{"style":1312},[3894],{"type":67,"value":1515},{"type":61,"tag":600,"props":3896,"children":3897},{"style":1340},[3898],{"type":67,"value":3899},"My Plugin",{"type":61,"tag":600,"props":3901,"children":3902},{"style":1312},[3903],{"type":67,"value":1515},{"type":61,"tag":600,"props":3905,"children":3906},{"style":1507},[3907],{"type":67,"value":3908},"] ",{"type":61,"tag":600,"props":3910,"children":3911},{"style":1312},[3912],{"type":67,"value":1332},{"type":61,"tag":600,"props":3914,"children":3915},{"style":1507},[3916],{"type":67,"value":3917}," MyComponent",{"type":61,"tag":600,"props":3919,"children":3920},{"style":1312},[3921],{"type":67,"value":3922},";",{"type":61,"tag":600,"props":3924,"children":3925},{"style":1303},[3926],{"type":67,"value":3927},"   \u002F\u002F matches plugin name\n",{"type":61,"tag":600,"props":3929,"children":3930},{"class":602,"line":621},[3931,3936,3940,3945,3949,3953,3957],{"type":61,"tag":600,"props":3932,"children":3933},{"style":1507},[3934],{"type":67,"value":3935},"globalThis",{"type":61,"tag":600,"props":3937,"children":3938},{"style":1312},[3939],{"type":67,"value":2514},{"type":61,"tag":600,"props":3941,"children":3942},{"style":1507},[3943],{"type":67,"value":3944},"AirflowPlugin ",{"type":61,"tag":600,"props":3946,"children":3947},{"style":1312},[3948],{"type":67,"value":1332},{"type":61,"tag":600,"props":3950,"children":3951},{"style":1507},[3952],{"type":67,"value":3917},{"type":61,"tag":600,"props":3954,"children":3955},{"style":1312},[3956],{"type":67,"value":3922},{"type":61,"tag":600,"props":3958,"children":3959},{"style":1303},[3960],{"type":67,"value":3961},"  \u002F\u002F fallback Airflow looks for\n",{"type":61,"tag":545,"props":3963,"children":3965},{"className":593,"code":3964,"language":21,"meta":553,"style":553},"class MyPlugin(AirflowPlugin):\n    name = \"my_plugin\"\n    fastapi_apps = [{\"app\": app, \"url_prefix\": \"\u002Fmy-plugin\", \"name\": \"My Plugin\"}]\n    react_apps = [\n        {\n            \"name\": \"My Plugin\",\n            \"bundle_url\": \"\u002Fmy-plugin\u002Fmy-app.js\",\n            \"destination\": \"nav\",\n            \"category\": \"browse\",\n            \"url_route\": \"my-plugin\",\n        }\n    ]\n",[3966],{"type":61,"tag":103,"props":3967,"children":3968},{"__ignoreMap":553},[3969,3976,3983,3991,3999,4006,4013,4021,4029,4037,4045,4052],{"type":61,"tag":600,"props":3970,"children":3971},{"class":602,"line":603},[3972],{"type":61,"tag":600,"props":3973,"children":3974},{},[3975],{"type":67,"value":741},{"type":61,"tag":600,"props":3977,"children":3978},{"class":602,"line":612},[3979],{"type":61,"tag":600,"props":3980,"children":3981},{},[3982],{"type":67,"value":750},{"type":61,"tag":600,"props":3984,"children":3985},{"class":602,"line":621},[3986],{"type":61,"tag":600,"props":3987,"children":3988},{},[3989],{"type":67,"value":3990},"    fastapi_apps = [{\"app\": app, \"url_prefix\": \"\u002Fmy-plugin\", \"name\": \"My Plugin\"}]\n",{"type":61,"tag":600,"props":3992,"children":3993},{"class":602,"line":630},[3994],{"type":61,"tag":600,"props":3995,"children":3996},{},[3997],{"type":67,"value":3998},"    react_apps = [\n",{"type":61,"tag":600,"props":4000,"children":4001},{"class":602,"line":639},[4002],{"type":61,"tag":600,"props":4003,"children":4004},{},[4005],{"type":67,"value":776},{"type":61,"tag":600,"props":4007,"children":4008},{"class":602,"line":648},[4009],{"type":61,"tag":600,"props":4010,"children":4011},{},[4012],{"type":67,"value":803},{"type":61,"tag":600,"props":4014,"children":4015},{"class":602,"line":658},[4016],{"type":61,"tag":600,"props":4017,"children":4018},{},[4019],{"type":67,"value":4020},"            \"bundle_url\": \"\u002Fmy-plugin\u002Fmy-app.js\",\n",{"type":61,"tag":600,"props":4022,"children":4023},{"class":602,"line":667},[4024],{"type":61,"tag":600,"props":4025,"children":4026},{},[4027],{"type":67,"value":4028},"            \"destination\": \"nav\",\n",{"type":61,"tag":600,"props":4030,"children":4031},{"class":602,"line":675},[4032],{"type":61,"tag":600,"props":4033,"children":4034},{},[4035],{"type":67,"value":4036},"            \"category\": \"browse\",\n",{"type":61,"tag":600,"props":4038,"children":4039},{"class":602,"line":684},[4040],{"type":61,"tag":600,"props":4041,"children":4042},{},[4043],{"type":67,"value":4044},"            \"url_route\": \"my-plugin\",\n",{"type":61,"tag":600,"props":4046,"children":4047},{"class":602,"line":692},[4048],{"type":61,"tag":600,"props":4049,"children":4050},{},[4051],{"type":67,"value":812},{"type":61,"tag":600,"props":4053,"children":4054},{"class":602,"line":701},[4055],{"type":61,"tag":600,"props":4056,"children":4057},{},[4058],{"type":67,"value":821},{"type":61,"tag":70,"props":4060,"children":4061},{},[4062,4064,4070],{"type":67,"value":4063},"The same bundle can be registered to multiple destinations by adding multiple entries — each needs a unique ",{"type":61,"tag":103,"props":4065,"children":4067},{"className":4066},[],[4068],{"type":67,"value":4069},"url_route",{"type":67,"value":4071},":",{"type":61,"tag":545,"props":4073,"children":4075},{"className":593,"code":4074,"language":21,"meta":553,"style":553},"react_apps = [\n    {\"name\": \"My Widget\", \"bundle_url\": \"\u002Fmy-plugin\u002Fwidget.js\", \"destination\": \"nav\",  \"url_route\": \"my-widget-nav\"},\n    {\"name\": \"My Widget\", \"bundle_url\": \"\u002Fmy-plugin\u002Fwidget.js\", \"destination\": \"dag\",  \"url_route\": \"my-widget-dag\"},\n]\n",[4076],{"type":61,"tag":103,"props":4077,"children":4078},{"__ignoreMap":553},[4079,4087,4095,4103],{"type":61,"tag":600,"props":4080,"children":4081},{"class":602,"line":603},[4082],{"type":61,"tag":600,"props":4083,"children":4084},{},[4085],{"type":67,"value":4086},"react_apps = [\n",{"type":61,"tag":600,"props":4088,"children":4089},{"class":602,"line":612},[4090],{"type":61,"tag":600,"props":4091,"children":4092},{},[4093],{"type":67,"value":4094},"    {\"name\": \"My Widget\", \"bundle_url\": \"\u002Fmy-plugin\u002Fwidget.js\", \"destination\": \"nav\",  \"url_route\": \"my-widget-nav\"},\n",{"type":61,"tag":600,"props":4096,"children":4097},{"class":602,"line":621},[4098],{"type":61,"tag":600,"props":4099,"children":4100},{},[4101],{"type":67,"value":4102},"    {\"name\": \"My Widget\", \"bundle_url\": \"\u002Fmy-plugin\u002Fwidget.js\", \"destination\": \"dag\",  \"url_route\": \"my-widget-dag\"},\n",{"type":61,"tag":600,"props":4104,"children":4105},{"class":602,"line":630},[4106],{"type":61,"tag":600,"props":4107,"children":4108},{},[4109],{"type":67,"value":4110},"]\n",{"type":61,"tag":76,"props":4112,"children":4113},{},[4114],{"type":61,"tag":70,"props":4115,"children":4116},{},[4117],{"type":67,"value":4118},"React app integration is experimental in Airflow 3.1. Interfaces may change in future releases.",{"type":61,"tag":287,"props":4120,"children":4121},{},[],{"type":61,"tag":291,"props":4123,"children":4125},{"id":4124},"step-7-environment-variables-and-deployment",[4126],{"type":67,"value":4127},"Step 7: Environment variables and deployment",{"type":61,"tag":70,"props":4129,"children":4130},{},[4131],{"type":67,"value":4132},"Never hardcode credentials:",{"type":61,"tag":545,"props":4134,"children":4136},{"className":593,"code":4135,"language":21,"meta":553,"style":553},"AIRFLOW_HOST = os.environ.get(\"MYPLUGIN_HOST\",     \"http:\u002F\u002Flocalhost:8080\")\nAIRFLOW_USER = os.environ.get(\"MYPLUGIN_USERNAME\", \"admin\")\nAIRFLOW_PASS = os.environ.get(\"MYPLUGIN_PASSWORD\", \"admin\")\n",[4137],{"type":61,"tag":103,"props":4138,"children":4139},{"__ignoreMap":553},[4140,4148,4156],{"type":61,"tag":600,"props":4141,"children":4142},{"class":602,"line":603},[4143],{"type":61,"tag":600,"props":4144,"children":4145},{},[4146],{"type":67,"value":4147},"AIRFLOW_HOST = os.environ.get(\"MYPLUGIN_HOST\",     \"http:\u002F\u002Flocalhost:8080\")\n",{"type":61,"tag":600,"props":4149,"children":4150},{"class":602,"line":612},[4151],{"type":61,"tag":600,"props":4152,"children":4153},{},[4154],{"type":67,"value":4155},"AIRFLOW_USER = os.environ.get(\"MYPLUGIN_USERNAME\", \"admin\")\n",{"type":61,"tag":600,"props":4157,"children":4158},{"class":602,"line":621},[4159],{"type":61,"tag":600,"props":4160,"children":4161},{},[4162],{"type":67,"value":4163},"AIRFLOW_PASS = os.environ.get(\"MYPLUGIN_PASSWORD\", \"admin\")\n",{"type":61,"tag":70,"props":4165,"children":4166},{},[4167],{"type":61,"tag":83,"props":4168,"children":4169},{},[4170],{"type":67,"value":4171},"Local Astro CLI:",{"type":61,"tag":545,"props":4173,"children":4176},{"className":4174,"code":4175,"language":67},[548],"# .env\nMYPLUGIN_HOST=http:\u002F\u002Flocalhost:8080\nMYPLUGIN_USERNAME=admin\nMYPLUGIN_PASSWORD=admin\n",[4177],{"type":61,"tag":103,"props":4178,"children":4179},{"__ignoreMap":553},[4180],{"type":67,"value":4175},{"type":61,"tag":545,"props":4182,"children":4186},{"className":4183,"code":4184,"language":4185,"meta":553,"style":553},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","astro dev restart              # required after any Python plugin change\n\n# Check logs by component (Astro CLI):\nastro dev logs --api-server    # FastAPI apps, external_views — plugin import errors show here\nastro dev logs --scheduler     # macros, timetables, listeners, operator links\nastro dev logs --dag-processor # DAG parsing errors\n\n# Non-Astro:\nairflow plugins                # CLI — lists all loaded plugins\n","bash",[4187],{"type":61,"tag":103,"props":4188,"children":4189},{"__ignoreMap":553},[4190,4214,4221,4229,4255,4280,4305,4312,4320],{"type":61,"tag":600,"props":4191,"children":4192},{"class":602,"line":603},[4193,4199,4204,4209],{"type":61,"tag":600,"props":4194,"children":4196},{"style":4195},"--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B",[4197],{"type":67,"value":4198},"astro",{"type":61,"tag":600,"props":4200,"children":4201},{"style":1340},[4202],{"type":67,"value":4203}," dev",{"type":61,"tag":600,"props":4205,"children":4206},{"style":1340},[4207],{"type":67,"value":4208}," restart",{"type":61,"tag":600,"props":4210,"children":4211},{"style":1303},[4212],{"type":67,"value":4213},"              # required after any Python plugin change\n",{"type":61,"tag":600,"props":4215,"children":4216},{"class":602,"line":612},[4217],{"type":61,"tag":600,"props":4218,"children":4219},{"emptyLinePlaceholder":652},[4220],{"type":67,"value":655},{"type":61,"tag":600,"props":4222,"children":4223},{"class":602,"line":621},[4224],{"type":61,"tag":600,"props":4225,"children":4226},{"style":1303},[4227],{"type":67,"value":4228},"# Check logs by component (Astro CLI):\n",{"type":61,"tag":600,"props":4230,"children":4231},{"class":602,"line":630},[4232,4236,4240,4245,4250],{"type":61,"tag":600,"props":4233,"children":4234},{"style":4195},[4235],{"type":67,"value":4198},{"type":61,"tag":600,"props":4237,"children":4238},{"style":1340},[4239],{"type":67,"value":4203},{"type":61,"tag":600,"props":4241,"children":4242},{"style":1340},[4243],{"type":67,"value":4244}," logs",{"type":61,"tag":600,"props":4246,"children":4247},{"style":1340},[4248],{"type":67,"value":4249}," --api-server",{"type":61,"tag":600,"props":4251,"children":4252},{"style":1303},[4253],{"type":67,"value":4254},"    # FastAPI apps, external_views — plugin import errors show here\n",{"type":61,"tag":600,"props":4256,"children":4257},{"class":602,"line":639},[4258,4262,4266,4270,4275],{"type":61,"tag":600,"props":4259,"children":4260},{"style":4195},[4261],{"type":67,"value":4198},{"type":61,"tag":600,"props":4263,"children":4264},{"style":1340},[4265],{"type":67,"value":4203},{"type":61,"tag":600,"props":4267,"children":4268},{"style":1340},[4269],{"type":67,"value":4244},{"type":61,"tag":600,"props":4271,"children":4272},{"style":1340},[4273],{"type":67,"value":4274}," --scheduler",{"type":61,"tag":600,"props":4276,"children":4277},{"style":1303},[4278],{"type":67,"value":4279},"     # macros, timetables, listeners, operator links\n",{"type":61,"tag":600,"props":4281,"children":4282},{"class":602,"line":648},[4283,4287,4291,4295,4300],{"type":61,"tag":600,"props":4284,"children":4285},{"style":4195},[4286],{"type":67,"value":4198},{"type":61,"tag":600,"props":4288,"children":4289},{"style":1340},[4290],{"type":67,"value":4203},{"type":61,"tag":600,"props":4292,"children":4293},{"style":1340},[4294],{"type":67,"value":4244},{"type":61,"tag":600,"props":4296,"children":4297},{"style":1340},[4298],{"type":67,"value":4299}," --dag-processor",{"type":61,"tag":600,"props":4301,"children":4302},{"style":1303},[4303],{"type":67,"value":4304}," # DAG parsing errors\n",{"type":61,"tag":600,"props":4306,"children":4307},{"class":602,"line":658},[4308],{"type":61,"tag":600,"props":4309,"children":4310},{"emptyLinePlaceholder":652},[4311],{"type":67,"value":655},{"type":61,"tag":600,"props":4313,"children":4314},{"class":602,"line":667},[4315],{"type":61,"tag":600,"props":4316,"children":4317},{"style":1303},[4318],{"type":67,"value":4319},"# Non-Astro:\n",{"type":61,"tag":600,"props":4321,"children":4322},{"class":602,"line":675},[4323,4327,4332],{"type":61,"tag":600,"props":4324,"children":4325},{"style":4195},[4326],{"type":67,"value":14},{"type":61,"tag":600,"props":4328,"children":4329},{"style":1340},[4330],{"type":67,"value":4331}," plugins",{"type":61,"tag":600,"props":4333,"children":4334},{"style":1303},[4335],{"type":67,"value":4336},"                # CLI — lists all loaded plugins\n",{"type":61,"tag":70,"props":4338,"children":4339},{},[4340],{"type":61,"tag":83,"props":4341,"children":4342},{},[4343],{"type":67,"value":4344},"Production Astronomer:",{"type":61,"tag":545,"props":4346,"children":4348},{"className":4183,"code":4347,"language":4185,"meta":553,"style":553},"astro deployment variable create --deployment-id \u003Cid> MYPLUGIN_HOST=https:\u002F\u002Fairflow.example.com\n",[4349],{"type":61,"tag":103,"props":4350,"children":4351},{"__ignoreMap":553},[4352],{"type":61,"tag":600,"props":4353,"children":4354},{"class":602,"line":603},[4355,4359,4364,4369,4374,4379,4384,4389,4394,4399],{"type":61,"tag":600,"props":4356,"children":4357},{"style":4195},[4358],{"type":67,"value":4198},{"type":61,"tag":600,"props":4360,"children":4361},{"style":1340},[4362],{"type":67,"value":4363}," deployment",{"type":61,"tag":600,"props":4365,"children":4366},{"style":1340},[4367],{"type":67,"value":4368}," variable",{"type":61,"tag":600,"props":4370,"children":4371},{"style":1340},[4372],{"type":67,"value":4373}," create",{"type":61,"tag":600,"props":4375,"children":4376},{"style":1340},[4377],{"type":67,"value":4378}," --deployment-id",{"type":61,"tag":600,"props":4380,"children":4381},{"style":1312},[4382],{"type":67,"value":4383}," \u003C",{"type":61,"tag":600,"props":4385,"children":4386},{"style":1340},[4387],{"type":67,"value":4388},"i",{"type":61,"tag":600,"props":4390,"children":4391},{"style":1507},[4392],{"type":67,"value":4393},"d",{"type":61,"tag":600,"props":4395,"children":4396},{"style":1312},[4397],{"type":67,"value":4398},">",{"type":61,"tag":600,"props":4400,"children":4401},{"style":1340},[4402],{"type":67,"value":4403}," MYPLUGIN_HOST=https:\u002F\u002Fairflow.example.com\n",{"type":61,"tag":70,"props":4405,"children":4406},{},[4407,4412],{"type":61,"tag":83,"props":4408,"children":4409},{},[4410],{"type":67,"value":4411},"Auto-reload during development",{"type":67,"value":4413}," (skips lazy loading):",{"type":61,"tag":545,"props":4415,"children":4418},{"className":4416,"code":4417,"language":67},[548],"AIRFLOW__CORE__LAZY_LOAD_PLUGINS=False\n",[4419],{"type":61,"tag":103,"props":4420,"children":4421},{"__ignoreMap":553},[4422],{"type":67,"value":4417},{"type":61,"tag":70,"props":4424,"children":4425},{},[4426,4431],{"type":61,"tag":83,"props":4427,"children":4428},{},[4429],{"type":67,"value":4430},"Cache busting for static files",{"type":67,"value":4432}," after deploy:",{"type":61,"tag":545,"props":4434,"children":4436},{"className":1291,"code":4435,"language":1293,"meta":553,"style":553},"\u003Cscript src=\"static\u002Fapp.js?v=20240315-1\">\u003C\u002Fscript>\n",[4437],{"type":61,"tag":103,"props":4438,"children":4439},{"__ignoreMap":553},[4440],{"type":61,"tag":600,"props":4441,"children":4442},{"class":602,"line":603},[4443,4447,4451,4455,4459,4463,4468,4472,4476,4480],{"type":61,"tag":600,"props":4444,"children":4445},{"style":1312},[4446],{"type":67,"value":1315},{"type":61,"tag":600,"props":4448,"children":4449},{"style":1318},[4450],{"type":67,"value":1386},{"type":61,"tag":600,"props":4452,"children":4453},{"style":1324},[4454],{"type":67,"value":1391},{"type":61,"tag":600,"props":4456,"children":4457},{"style":1312},[4458],{"type":67,"value":1332},{"type":61,"tag":600,"props":4460,"children":4461},{"style":1312},[4462],{"type":67,"value":1337},{"type":61,"tag":600,"props":4464,"children":4465},{"style":1340},[4466],{"type":67,"value":4467},"static\u002Fapp.js?v=20240315-1",{"type":61,"tag":600,"props":4469,"children":4470},{"style":1312},[4471],{"type":67,"value":1337},{"type":61,"tag":600,"props":4473,"children":4474},{"style":1312},[4475],{"type":67,"value":1413},{"type":61,"tag":600,"props":4477,"children":4478},{"style":1318},[4479],{"type":67,"value":1386},{"type":61,"tag":600,"props":4481,"children":4482},{"style":1312},[4483],{"type":67,"value":1422},{"type":61,"tag":70,"props":4485,"children":4486},{},[4487,4492,4494,4499],{"type":61,"tag":83,"props":4488,"children":4489},{},[4490],{"type":67,"value":4491},"Verify the plugin loaded",{"type":67,"value":4493},": open ",{"type":61,"tag":83,"props":4495,"children":4496},{},[4497],{"type":67,"value":4498},"Admin > Plugins",{"type":67,"value":4500}," in the Airflow UI.",{"type":61,"tag":70,"props":4502,"children":4503},{},[4504,4509],{"type":61,"tag":83,"props":4505,"children":4506},{},[4507],{"type":67,"value":4508},"OpenAPI docs",{"type":67,"value":4510}," are auto-generated for FastAPI plugins:",{"type":61,"tag":2193,"props":4512,"children":4513},{},[4514,4525],{"type":61,"tag":215,"props":4515,"children":4516},{},[4517,4519],{"type":67,"value":4518},"Swagger UI: ",{"type":61,"tag":103,"props":4520,"children":4522},{"className":4521},[],[4523],{"type":67,"value":4524},"{AIRFLOW_HOST}\u002F{url_prefix}\u002Fdocs",{"type":61,"tag":215,"props":4526,"children":4527},{},[4528,4530],{"type":67,"value":4529},"OpenAPI JSON: ",{"type":61,"tag":103,"props":4531,"children":4533},{"className":4532},[],[4534],{"type":67,"value":4535},"{AIRFLOW_HOST}\u002F{url_prefix}\u002Fopenapi.json",{"type":61,"tag":287,"props":4537,"children":4538},{},[],{"type":61,"tag":291,"props":4540,"children":4542},{"id":4541},"common-pitfalls",[4543],{"type":67,"value":4544},"Common pitfalls",{"type":61,"tag":303,"props":4546,"children":4547},{},[4548,4569],{"type":61,"tag":307,"props":4549,"children":4550},{},[4551],{"type":61,"tag":311,"props":4552,"children":4553},{},[4554,4559,4564],{"type":61,"tag":315,"props":4555,"children":4556},{},[4557],{"type":67,"value":4558},"Problem",{"type":61,"tag":315,"props":4560,"children":4561},{},[4562],{"type":67,"value":4563},"Cause",{"type":61,"tag":315,"props":4565,"children":4566},{},[4567],{"type":67,"value":4568},"Fix",{"type":61,"tag":331,"props":4570,"children":4571},{},[4572,4613,4654,4682,4700,4736,4758,4776,4808],{"type":61,"tag":311,"props":4573,"children":4574},{},[4575,4580,4597],{"type":61,"tag":338,"props":4576,"children":4577},{},[4578],{"type":67,"value":4579},"Nav link goes to 404",{"type":61,"tag":338,"props":4581,"children":4582},{},[4583,4585,4591,4592],{"type":67,"value":4584},"Leading ",{"type":61,"tag":103,"props":4586,"children":4588},{"className":4587},[],[4589],{"type":67,"value":4590},"\u002F",{"type":67,"value":566},{"type":61,"tag":103,"props":4593,"children":4595},{"className":4594},[],[4596],{"type":67,"value":192},{"type":61,"tag":338,"props":4598,"children":4599},{},[4600,4605,4607],{"type":61,"tag":103,"props":4601,"children":4603},{"className":4602},[],[4604],{"type":67,"value":1103},{"type":67,"value":4606}," not ",{"type":61,"tag":103,"props":4608,"children":4610},{"className":4609},[],[4611],{"type":67,"value":4612},"\"\u002Fmy-plugin\u002Fui\"",{"type":61,"tag":311,"props":4614,"children":4615},{},[4616,4621,4638],{"type":61,"tag":338,"props":4617,"children":4618},{},[4619],{"type":67,"value":4620},"Nav icon not showing",{"type":61,"tag":338,"props":4622,"children":4623},{},[4624,4626,4631,4632],{"type":67,"value":4625},"Missing ",{"type":61,"tag":103,"props":4627,"children":4629},{"className":4628},[],[4630],{"type":67,"value":4590},{"type":67,"value":566},{"type":61,"tag":103,"props":4633,"children":4635},{"className":4634},[],[4636],{"type":67,"value":4637},"icon",{"type":61,"tag":338,"props":4639,"children":4640},{},[4641,4646,4648],{"type":61,"tag":103,"props":4642,"children":4644},{"className":4643},[],[4645],{"type":67,"value":4637},{"type":67,"value":4647}," takes an absolute path: ",{"type":61,"tag":103,"props":4649,"children":4651},{"className":4650},[],[4652],{"type":67,"value":4653},"\"\u002Fmy-plugin\u002Fstatic\u002Ficon.svg\"",{"type":61,"tag":311,"props":4655,"children":4656},{},[4657,4662,4672],{"type":61,"tag":338,"props":4658,"children":4659},{},[4660],{"type":67,"value":4661},"Event loop freezes under load",{"type":61,"tag":338,"props":4663,"children":4664},{},[4665,4667],{"type":67,"value":4666},"Sync SDK called directly in ",{"type":61,"tag":103,"props":4668,"children":4670},{"className":4669},[],[4671],{"type":67,"value":1736},{"type":61,"tag":338,"props":4673,"children":4674},{},[4675,4677],{"type":67,"value":4676},"Wrap with ",{"type":61,"tag":103,"props":4678,"children":4680},{"className":4679},[],[4681],{"type":67,"value":257},{"type":61,"tag":311,"props":4683,"children":4684},{},[4685,4690,4695],{"type":61,"tag":338,"props":4686,"children":4687},{},[4688],{"type":67,"value":4689},"401 errors after 1 hour",{"type":61,"tag":338,"props":4691,"children":4692},{},[4693],{"type":67,"value":4694},"JWT expires with no refresh",{"type":61,"tag":338,"props":4696,"children":4697},{},[4698],{"type":67,"value":4699},"Use the 5-minute pre-expiry refresh pattern",{"type":61,"tag":311,"props":4701,"children":4702},{},[4703,4713,4718],{"type":61,"tag":338,"props":4704,"children":4705},{},[4706,4711],{"type":61,"tag":103,"props":4707,"children":4709},{"className":4708},[],[4710],{"type":67,"value":588},{"type":67,"value":4712}," raises on startup",{"type":61,"tag":338,"props":4714,"children":4715},{},[4716],{"type":67,"value":4717},"Directory missing",{"type":61,"tag":338,"props":4719,"children":4720},{},[4721,4723,4728,4729,4734],{"type":67,"value":4722},"Create ",{"type":61,"tag":103,"props":4724,"children":4726},{"className":4725},[],[4727],{"type":67,"value":278},{"type":67,"value":272},{"type":61,"tag":103,"props":4730,"children":4732},{"className":4731},[],[4733],{"type":67,"value":270},{"type":67,"value":4735}," before starting",{"type":61,"tag":311,"props":4737,"children":4738},{},[4739,4744,4749],{"type":61,"tag":338,"props":4740,"children":4741},{},[4742],{"type":67,"value":4743},"Plugin not showing up",{"type":61,"tag":338,"props":4745,"children":4746},{},[4747],{"type":67,"value":4748},"Python file changed without restart",{"type":61,"tag":338,"props":4750,"children":4751},{},[4752],{"type":61,"tag":103,"props":4753,"children":4755},{"className":4754},[],[4756],{"type":67,"value":4757},"astro dev restart",{"type":61,"tag":311,"props":4759,"children":4760},{},[4761,4766,4771],{"type":61,"tag":338,"props":4762,"children":4763},{},[4764],{"type":67,"value":4765},"Endpoints accessible without login",{"type":61,"tag":338,"props":4767,"children":4768},{},[4769],{"type":67,"value":4770},"FastAPI apps are not auto-authenticated",{"type":61,"tag":338,"props":4772,"children":4773},{},[4774],{"type":67,"value":4775},"Add FastAPI security (e.g. OAuth2, API key) if endpoints must be private",{"type":61,"tag":311,"props":4777,"children":4778},{},[4779,4784,4789],{"type":61,"tag":338,"props":4780,"children":4781},{},[4782],{"type":67,"value":4783},"Middleware affecting wrong routes",{"type":61,"tag":338,"props":4785,"children":4786},{},[4787],{"type":67,"value":4788},"Middleware applies to all API traffic",{"type":61,"tag":338,"props":4790,"children":4791},{},[4792,4794,4800,4802],{"type":67,"value":4793},"Filter by ",{"type":61,"tag":103,"props":4795,"children":4797},{"className":4796},[],[4798],{"type":67,"value":4799},"request.url.path",{"type":67,"value":4801}," inside ",{"type":61,"tag":103,"props":4803,"children":4805},{"className":4804},[],[4806],{"type":67,"value":4807},"dispatch()",{"type":61,"tag":311,"props":4809,"children":4810},{},[4811,4823,4833],{"type":61,"tag":338,"props":4812,"children":4813},{},[4814,4816,4821],{"type":67,"value":4815},"JS ",{"type":61,"tag":103,"props":4817,"children":4819},{"className":4818},[],[4820],{"type":67,"value":200},{"type":67,"value":4822}," breaks on Astro",{"type":61,"tag":338,"props":4824,"children":4825},{},[4826,4828],{"type":67,"value":4827},"Absolute path in ",{"type":61,"tag":103,"props":4829,"children":4831},{"className":4830},[],[4832],{"type":67,"value":200},{"type":61,"tag":338,"props":4834,"children":4835},{},[4836,4838],{"type":67,"value":4837},"Always use relative paths: ",{"type":61,"tag":103,"props":4839,"children":4841},{"className":4840},[],[4842],{"type":67,"value":4843},"fetch('api\u002Fdags')",{"type":61,"tag":287,"props":4845,"children":4846},{},[],{"type":61,"tag":291,"props":4848,"children":4850},{"id":4849},"references",[4851],{"type":67,"value":4852},"References",{"type":61,"tag":2193,"props":4854,"children":4855},{},[4856,4866,4877],{"type":61,"tag":215,"props":4857,"children":4858},{},[4859],{"type":61,"tag":2433,"props":4860,"children":4863},{"href":4861,"rel":4862},"https:\u002F\u002Fairflow.apache.org\u002Fdocs\u002Fapache-airflow\u002Fstable\u002Fadministration-and-deployment\u002Fplugins.html",[2437],[4864],{"type":67,"value":4865},"Airflow plugins documentation",{"type":61,"tag":215,"props":4867,"children":4868},{},[4869,4875],{"type":61,"tag":2433,"props":4870,"children":4872},{"href":2435,"rel":4871},[2437],[4873],{"type":67,"value":4874},"Airflow REST API reference",{"type":67,"value":4876}," — full endpoint list with SDK class\u002Fmethod names",{"type":61,"tag":215,"props":4878,"children":4879},{},[4880],{"type":61,"tag":2433,"props":4881,"children":4884},{"href":4882,"rel":4883},"https:\u002F\u002Fwww.astronomer.io\u002Fdocs\u002Flearn\u002Fusing-airflow-plugins",[2437],[4885],{"type":67,"value":4886},"Astronomer: Using Airflow plugins",{"type":61,"tag":4888,"props":4889,"children":4890},"style",{},[4891],{"type":67,"value":4892},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"items":4894,"total":893},[4895,4911,4923,4930,4944,4961,4974,4987,5002,5016,5026,5039],{"slug":14,"name":14,"fn":4896,"description":4897,"org":4898,"tags":4899,"stars":25,"repoUrl":26,"updatedAt":4910},"manage and troubleshoot Airflow via CLI","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 task logs, diagnosing failures, debugging import and parse errors, checking connections, variables and pools, exploring the REST API, and monitoring health (for example \"trigger a pipeline\", \"retry a run\", \"list connections\", \"check Airflow health\", \"why did my DAG fail\"). This is the entrypoint that routes to sibling skills for authoring, testing, deploying, and migrating Airflow 2 to 3. Not for warehouse\u002FSQL analytics on Airflow metadata tables (use analyzing-data); for deep root-cause reports use debugging-dags or airflow-investigation.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4900,4901,4904,4907],{"name":13,"slug":14,"type":15},{"name":4902,"slug":4903,"type":15},"CLI","cli",{"name":4905,"slug":4906,"type":15},"Data Pipeline","data-pipeline",{"name":4908,"slug":4909,"type":15},"Debugging","debugging","2026-04-06T18:01:43.992997",{"slug":4912,"name":4912,"fn":4913,"description":4914,"org":4915,"tags":4916,"stars":25,"repoUrl":26,"updatedAt":4922},"airflow-hitl","add human-in-the-loop steps to Airflow DAGs","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 form input mid-run; also on mentions of ApprovalOperator, HITLOperator, HITLBranchOperator, HITLEntryOperator, or HITLTrigger. Requires Airflow 3.1+. Not for AI\u002FLLM task calls (see migrating-ai-sdk-to-common-ai).",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4917,4918,4921],{"name":13,"slug":14,"type":15},{"name":4919,"slug":4920,"type":15},"Approvals","approvals",{"name":4905,"slug":4906,"type":15},"2026-04-06T18:01:46.758548",{"slug":4,"name":4,"fn":5,"description":6,"org":4924,"tags":4925,"stars":25,"repoUrl":26,"updatedAt":27},{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4926,4927,4928,4929],{"name":13,"slug":14,"type":15},{"name":17,"slug":18,"type":15},{"name":20,"slug":21,"type":15},{"name":23,"slug":24,"type":15},{"slug":4931,"name":4931,"fn":4932,"description":4933,"org":4934,"tags":4935,"stars":25,"repoUrl":26,"updatedAt":4943},"airflow-state-store","persist Airflow task and asset state","Persists task and asset state across retries and DAG runs using Airflow 3.3's AIP-103 key\u002Fvalue 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 retries, job IDs surviving worker crashes, watermarks, asset metadata, resumable tasks, crash-safe operators, or \"what's new in Airflow 3.3\". Also use proactively when reading a DAG that uses Variables or XCom for intra-task coordination state — flag the anti-pattern and recommend task_state_store or asset_state_store instead. Requires Airflow 3.3+.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4936,4937,4939,4940],{"name":13,"slug":14,"type":15},{"name":4938,"slug":39,"type":15},"Data Engineering",{"name":4905,"slug":4906,"type":15},{"name":4941,"slug":4942,"type":15},"Operations","operations","2026-07-07T06:43:11.160671",{"slug":4945,"name":4945,"fn":4946,"description":4947,"org":4948,"tags":4949,"stars":25,"repoUrl":26,"updatedAt":4960},"analyzing-data","query data warehouses for business questions","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 me Z\", \"find customers\", \"what is the count\").",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4950,4953,4956,4957],{"name":4951,"slug":4952,"type":15},"Analytics","analytics",{"name":4954,"slug":4955,"type":15},"Data Analysis","data-analysis",{"name":4938,"slug":39,"type":15},{"name":4958,"slug":4959,"type":15},"SQL","sql","2026-04-06T18:01:49.599775",{"slug":4962,"name":4962,"fn":4963,"description":4964,"org":4965,"tags":4966,"stars":25,"repoUrl":26,"updatedAt":4973},"annotating-task-lineage","annotate Airflow tasks with data lineage","Annotate Airflow tasks with data lineage using inlets and outlets. Use when the user wants to add lineage metadata to tasks, specify input\u002Foutput datasets, or enable lineage tracking for operators without built-in OpenLineage extraction.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4967,4968,4969,4970],{"name":13,"slug":14,"type":15},{"name":4938,"slug":39,"type":15},{"name":4905,"slug":4906,"type":15},{"name":4971,"slug":4972,"type":15},"Observability","observability","2026-04-06T18:02:03.487365",{"slug":4975,"name":4975,"fn":4976,"description":4977,"org":4978,"tags":4979,"stars":25,"repoUrl":26,"updatedAt":4986},"authoring-dags","author Airflow DAGs","Workflow and best practices for writing Apache Airflow DAGs. Use when creating a new DAG, write pipeline code, handling questions about DAG patterns and conventions or extending an existing DAG with a follow-up\u002Fdownstream task. ANY request shaped like 'add a DAG named X', 'write a pipeline', 'add a task that runs after Y', or 'extend the DAG'. For testing and debugging DAGs, see the testing-dags skill.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4980,4981,4982,4985],{"name":13,"slug":14,"type":15},{"name":4905,"slug":4906,"type":15},{"name":4983,"slug":4984,"type":15},"ETL","etl",{"name":20,"slug":21,"type":15},"2026-04-06T18:01:52.679888",{"slug":4988,"name":4988,"fn":4989,"description":4990,"org":4991,"tags":4992,"stars":25,"repoUrl":26,"updatedAt":5001},"authoring-go-sdk-tasks","implement Airflow tasks in Go","Writes Airflow task logic in Go using the Airflow Go SDK. Use when the user wants to implement Airflow tasks in Go, asks about `BundleProvider`\u002F`RegisterDags`, the `bundlev1` Registry\u002FDag interfaces, registering Go tasks (`AddTask`\u002F`AddTaskWithName`), dependency injection by parameter type (`context.Context`, `sdk.TIRunContext`, `*slog.Logger`, `sdk.Client`), or reading connections\u002Fvariables\u002FXComs from Go. This skill covers the Go-specific native API; the shared Python-stub pattern and conceptual model live in authoring-language-sdk-tasks. For building\u002Fpacking\u002Fshipping the bundle see deploying-go-sdk-bundles; for coordinator config see configuring-airflow-language-sdks.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4993,4994,4997,4998],{"name":13,"slug":14,"type":15},{"name":4995,"slug":4996,"type":15},"API Development","api-development",{"name":4905,"slug":4906,"type":15},{"name":4999,"slug":5000,"type":15},"Go","go","2026-07-11T05:39:13.552213",{"slug":5003,"name":5003,"fn":5004,"description":5005,"org":5006,"tags":5007,"stars":25,"repoUrl":26,"updatedAt":5015},"authoring-java-sdk-tasks","implement Airflow tasks in Java","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\u002FJVM, asks about `@Builder.Dag`\u002F`@Builder.Task`\u002F`@Builder.XCom`, the `Task`\u002F`BundleBuilder` interfaces, reading connections\u002Fvariables\u002FXComs 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\u002Fshipping the bundle see deploying-java-sdk-bundles; for coordinator config see configuring-airflow-language-sdks.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[5008,5009,5012],{"name":4905,"slug":4906,"type":15},{"name":5010,"slug":5011,"type":15},"Engineering","engineering",{"name":5013,"slug":5014,"type":15},"Java","java","2026-07-18T05:48:13.374003",{"slug":5017,"name":5017,"fn":5018,"description":5019,"org":5020,"tags":5021,"stars":25,"repoUrl":26,"updatedAt":5025},"authoring-language-sdk-tasks","implement non-Python Airflow tasks","The language-neutral foundation for Airflow language SDKs — implement task logic in a non-Python language while the DAG stays in Python. Use when the user wants to run an Airflow task in another language (Java, Kotlin, Go, or other JVM\u002Fnative languages), asks how the Python `@task.stub` pairs with native task code, how task\u002FDAG IDs must match across the two sides, how data passes via XCom as JSON, or which language SDKs exist. This skill owns the shared Python-stub pattern and conceptual model; for a specific language's native API, build, and runtime, use that language's skill (e.g. authoring-java-sdk-tasks, authoring-go-sdk-tasks).",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[5022,5023,5024],{"name":13,"slug":14,"type":15},{"name":4905,"slug":4906,"type":15},{"name":5010,"slug":5011,"type":15},"2026-07-18T05:11:54.496539",{"slug":5027,"name":5027,"fn":5028,"description":5029,"org":5030,"tags":5031,"stars":25,"repoUrl":26,"updatedAt":5038},"blueprint","build reusable Airflow task group templates","Define reusable Airflow task group templates with Pydantic validation and compose DAGs from YAML. Use when creating blueprint templates, composing DAGs from YAML, validating configurations, or enabling no-code DAG authoring for non-engineers.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[5032,5033,5034,5035],{"name":13,"slug":14,"type":15},{"name":4905,"slug":4906,"type":15},{"name":4983,"slug":4984,"type":15},{"name":5036,"slug":5037,"type":15},"Templates","templates","2026-04-06T18:01:45.361425",{"slug":5040,"name":5040,"fn":5041,"description":5042,"org":5043,"tags":5044,"stars":25,"repoUrl":26,"updatedAt":5051},"checking-freshness","check data freshness in warehouses","Quick data freshness check. Use when the user asks if data is up to date, when a table was last updated, if data is stale, or needs to verify data currency before using it.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[5045,5046,5047,5050],{"name":13,"slug":14,"type":15},{"name":4938,"slug":39,"type":15},{"name":5048,"slug":5049,"type":15},"Data Quality","data-quality",{"name":4983,"slug":4984,"type":15},"2026-04-06T18:02:02.138565",{"items":5053,"total":893},[5054,5061,5067,5074,5081,5088,5095],{"slug":14,"name":14,"fn":4896,"description":4897,"org":5055,"tags":5056,"stars":25,"repoUrl":26,"updatedAt":4910},{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[5057,5058,5059,5060],{"name":13,"slug":14,"type":15},{"name":4902,"slug":4903,"type":15},{"name":4905,"slug":4906,"type":15},{"name":4908,"slug":4909,"type":15},{"slug":4912,"name":4912,"fn":4913,"description":4914,"org":5062,"tags":5063,"stars":25,"repoUrl":26,"updatedAt":4922},{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[5064,5065,5066],{"name":13,"slug":14,"type":15},{"name":4919,"slug":4920,"type":15},{"name":4905,"slug":4906,"type":15},{"slug":4,"name":4,"fn":5,"description":6,"org":5068,"tags":5069,"stars":25,"repoUrl":26,"updatedAt":27},{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[5070,5071,5072,5073],{"name":13,"slug":14,"type":15},{"name":17,"slug":18,"type":15},{"name":20,"slug":21,"type":15},{"name":23,"slug":24,"type":15},{"slug":4931,"name":4931,"fn":4932,"description":4933,"org":5075,"tags":5076,"stars":25,"repoUrl":26,"updatedAt":4943},{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[5077,5078,5079,5080],{"name":13,"slug":14,"type":15},{"name":4938,"slug":39,"type":15},{"name":4905,"slug":4906,"type":15},{"name":4941,"slug":4942,"type":15},{"slug":4945,"name":4945,"fn":4946,"description":4947,"org":5082,"tags":5083,"stars":25,"repoUrl":26,"updatedAt":4960},{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[5084,5085,5086,5087],{"name":4951,"slug":4952,"type":15},{"name":4954,"slug":4955,"type":15},{"name":4938,"slug":39,"type":15},{"name":4958,"slug":4959,"type":15},{"slug":4962,"name":4962,"fn":4963,"description":4964,"org":5089,"tags":5090,"stars":25,"repoUrl":26,"updatedAt":4973},{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[5091,5092,5093,5094],{"name":13,"slug":14,"type":15},{"name":4938,"slug":39,"type":15},{"name":4905,"slug":4906,"type":15},{"name":4971,"slug":4972,"type":15},{"slug":4975,"name":4975,"fn":4976,"description":4977,"org":5096,"tags":5097,"stars":25,"repoUrl":26,"updatedAt":4986},{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[5098,5099,5100,5101],{"name":13,"slug":14,"type":15},{"name":4905,"slug":4906,"type":15},{"name":4983,"slug":4984,"type":15},{"name":20,"slug":21,"type":15}]