astro-airflow · diff
v1.0.0 to v1.2.0
178 added, 187 removed. Audit A to A.
---
name: astro-airflow
- description: "Inspect and debug Airflow on Astronomer (Astro) deployments - fetch DAG runs, task instance logs, container logs, env vars, and deployment state without installing an MCP plugin. Use when the user mentions Astro/Astronomer, asks about DAG runs or task logs on staging/prod, says 'check the deployment', references `astro deployment`, `make airflow`, an Astro deployment ID, or a *.astronomer.run URL. Pairs the official `astro` CLI for platform ops with direct Airflow REST API calls for DAG-level data."
+ description: "Inspect and debug Airflow on Astronomer (Astro) deployments - DAG runs, task logs, container logs, env vars, and deployment state. Use when the user mentions Astro/Astronomer, asks about DAG runs or task logs on staging/prod, says 'check the deployment', references `astro deployment`, an Astro deployment ID, or a *.astronomer.run URL. Prefers the `af` CLI for DAG-level data and `astro` CLI for platform ops; curl is the fallback. Local astro dev -> vd:managing-astro-local-env. YAML DAG authoring -> vd:dag-factory. 'use Otto' / Airflow 2→3 upgrades -> vd:delegating-to-otto."
license: MIT
metadata:
author: vanducng
- version: "1.0.0"
- upstream: "https://docs.astronomer.io/astro/cli/overview"
+ version: "1.2.0"
+ upstream: "https://github.com/astronomer/agents/tree/main/skills/airflow"
---
# astro-airflow
- Read-only debugging surface for Airflow on Astro. No MCP plugin install required - uses `astro` CLI for what it exposes (container logs, env vars, deployment metadata) and `curl` against the deployment's Airflow REST API for what it doesn't (DAG runs, task instance logs).
-
- ## When to use
+ Read-only debugging surface for Airflow on Astro. Pair three tools:
- - User asks "what failed in `<dag_id>`" or "why is `<dag_id>` stuck" on staging/prod
- - User says "check the deployment logs", "fetch task log", "clear failed tasks" on Astro
- - User references an Astro deployment ID (e.g. `cmgjr4hyl001l01lrepyhz604`) or a `*.astronomer.run/<id>` URL
- - A repo's CLAUDE.md mentions Astro deployments, `make airflow CMD=...`, or `astro dev start`
- - Need to compare staging vs prod env vars / connections / variables
- - Debugging a CI deploy that hit Astronomer
+ | Need | Tool |
+ |---|---|
+ | DAG runs, task logs, import errors, health, connections, variables, pools | `af` (Airflow REST wrapper) |
+ | Container logs, deployment inspect, env vars, hibernate/wake | `astro deployment ...` |
+ | Local `astro dev start` / parse / scheduler restart | `vd:managing-astro-local-env` |
+ | Create or edit YAML DAGs | `vd:dag-factory` |
+ | User says "use Otto", long audit, Airflow 2→3 upgrade | `vd:delegating-to-otto` |
+ | Deep RCA after logs are in hand | `vd:debug` then `vd:fix` |
+ | `af` not installed or no remote instance configured | curl against `/api/v2/` (Airflow 3) or `/api/v1/` (Airflow 2) |
- **When NOT to use:** local-only Airflow questions where `make airflow CMD=...` or the local Astro container suffices - use those directly. This skill is for *remote* (staging/prod) inspection.
+ **When NOT to use:** local-only Airflow (`astro dev start`, parse, pytest) - that is `vd:managing-astro-local-env`. YAML authoring is `vd:dag-factory`. This skill is remote (staging/prod) inspection, plus wiring `af` at those URLs.
## Prerequisites
- - `astro` CLI installed and logged in (`astro login <org-hostname>`) - verify with `astro context list`
- - `curl` and `jq` (both standard)
- - A **Deployment API token** for the target deployment, stored in gopass. Mint via Astro UI → Deployment → Access → API Tokens. Use a custom least-privilege role with `deployment.get` + `deployment.airflow.*.get` permissions for read-only debugging.
- - Deployment **webserver URL** (e.g. `https://<org>.astronomer.run/<deployment-short-id>/`) - find via `astro deployment inspect <deployment-id> --key metadata.airflow_api_url`
-
- ## Core: `astro` CLI for platform ops
+ - `astro` CLI ≥ 1.42, logged in (`astro login`; verify with `astro context list`)
+ - `curl` + `jq`
+ - `af` optional but preferred: `uv tool install astro-airflow-mcp` (one-shot: `uvx --from astro-airflow-mcp af`)
+ - A Deployment / Workspace / Organization API token in gopass for curl fallback. Mint via Astro UI → Deployment → Access → API Tokens. Least-privilege: `deployment.get` + `deployment.airflow.*.get`.
- ### Discover deployments
+ Always resolve the live deployment ID first. IDs in project docs go stale.
```bash
- astro deployment list # all deployments in current workspace
- astro deployment inspect <deployment-id> # full deployment metadata (JSON)
+ astro deployment list
astro deployment inspect <deployment-id> --key metadata.airflow_api_url
- astro deployment inspect <deployment-id> --key metadata.workload_identity
```
- ### Container logs (scheduler / api-server / triggerer / workers)
+ ## Choose the interface
- Container-level logs - Python tracebacks, scheduler errors, OOM kills. **Not** per-task logs. Component is a **boolean flag** (`--scheduler`, `--apiserver`, `--triggerer`, `--workers`, `--webserver`), not `--component <name>`.
+ 1. **Named Otto / upgrade / long investigation** → `vd:delegating-to-otto`.
+ 2. **`af` on PATH (or `uvx --from astro-airflow-mcp af`) and a configured instance** → use `af`. See [references/af.md](references/af.md).
+ 3. **Platform / container logs / env vars** → `astro deployment ...` below.
+ 4. **Otherwise** → curl fallback.
+ `af instance discover` **creates API tokens in Astro Cloud**. Always `--dry-run` first and get explicit approval before a real discover.
+
```bash
- astro deployment logs <deployment-id> --scheduler # default last 500 lines
- astro deployment logs <deployment-id> --scheduler --log-count 100 # tail N lines
- astro deployment logs <deployment-id> --scheduler --keyword "ImportError"
- astro deployment logs <deployment-id> --scheduler --error # level filter only
- astro deployment logs <deployment-id> --apiserver # Airflow 3.x API server
- astro deployment logs <deployment-id> --webserver # Airflow 2.x web UI
- astro deployment logs <deployment-id> --triggerer
- astro deployment logs <deployment-id> --workers --keyword "OOMKilled"
+ uvx --from astro-airflow-mcp af instance list
+ uvx --from astro-airflow-mcp af instance discover --dry-run
+ # only after the user says yes:
+ # uvx --from astro-airflow-mcp af instance discover astro
+ uvx --from astro-airflow-mcp af instance use <name>
```
- **Limitation:** Cannot combine `--error` (level) and `--keyword` (text) in the same call - pick one. To do both, run `--keyword "ERROR"` and grep the output for what you actually want.
-
- Use these when:
- - Scheduler isn't picking up DAGs (look for parse errors) - `--scheduler --keyword "ImportError\|Broken DAG"`
- - Triggerer is crashing (async task issues) - `--triggerer --error`
- - Workers OOM - `--workers --keyword "OOMKilled"`
- - DAG import errors not visible in UI - `--scheduler --keyword "Broken DAG"`
-
- ### Environment variables
+ Add by hand without persisting the secret. `af instance add --token` writes the value into `~/.astro/config.yaml`. Prefer a `${VAR}` reference or a one-shot env:
```bash
- astro deployment variable list --deployment-id <id> # list all (values redacted by default)
- astro deployment variable list --deployment-id <id> -s # include secret values (sensitive!)
- astro deployment variable create --deployment-id <id> KEY=value # create or update
- astro deployment variable update --deployment-id <id> KEY=value
- astro deployment variable copy --source-id <staging> --target-id <prod> # promote
+ export AIRFLOW_AUTH_TOKEN="$(gopass show -o <path/to/deployment-token>)"
+ export AIRFLOW_API_URL="https://<org>.astronomer.run/<short-id>"
+ uvx --from astro-airflow-mcp af dags list
```
- ### Deploy state
+ Persistent instance (token stays in the environment, not the file):
- ```bash
- astro deployment hibernate <id> --force # pause compute
- astro deployment wake-up <id> # resume
- astro deployment pool list --deployment-id <id> # worker pool sizing
+ ```yaml
+ # ~/.astro/config.yaml (or project .astro/config.yaml)
+ instances:
+ - name: staging
+ url: https://<org>.astronomer.run/<short-id>
+ auth:
+ token: ${AIRFLOW_AUTH_TOKEN}
```
- ## Core: Airflow REST API for DAG runs + task logs
+ If `af` prints `reading from the legacy ~/.af/config.yaml`, tell the user `af migrate` exists; do not run it unasked.
- Astro CLI does not expose per-task logs or structured DAG run state. Hit the deployment's Airflow REST API directly. **Airflow 3.x = `/api/v2/`**, **Airflow 2.x = `/api/v1/`**.
+ ## Platform ops: `astro` CLI
- ### Setup helpers (use once per session)
+ ### Container logs
- Any Astro API token works as `Authorization: Bearer` for a deployment's Airflow API - **Deployment**
- (narrowest, preferred for prod automation), **Workspace** (any deployment in the workspace), or
- **Organization**. Verified June 2026: the CNB repo's `ASTRO_WORKSPACE_API_KEY` (in `cnb-ds-astro/.env`)
- hits prod's `/api/v2/...` fine. The same value works for the `astro` CLI via `export ASTRO_API_TOKEN=...`.
+ Component is a **boolean flag or `--component <name>`** (CLI 1.45+). Airflow 3.3+ parse/import lives on **`--dag-processor`**, not only `--scheduler`. Airflow 3.x API is `--apiserver`; `--webserver` is Airflow 2.x.
- ```bash
- # Token: gopass deployment token, OR the repo's workspace key (never echo it)
- export ASTRO_TOKEN="$(gopass show -o <path/to/deployment-token>)"
- # export ASTRO_TOKEN="$(grep -E '^ASTRO_WORKSPACE_API_KEY=' /path/to/repo/.env | cut -d= -f2-)"
+ `--keyword` is an **exact phrase**, not a regex. Do **not** combine `--error`/`--warn`/`--info` with each other or with `--keyword` - the CLI prints usage and exits. Official help examples that show `--error --info` are wrong on 1.45.x.
- # Deployment webserver URL
- export AF_URL="https://<org>.astronomer.run/<deployment-short-id>"
+ `--error` is not a reliable level filter: it can return `[info]` lines whose text contains "error" (DAG ids like `el_twilio__error_code`). Prefer `--keyword "ImportError"` on `--dag-processor`.
- # Reusable curl wrapper
- af() { curl -fsSL -H "Authorization: Bearer ${ASTRO_TOKEN}" "${AF_URL}$1"; }
+ ```bash
+ astro deployment logs <deployment-id> --scheduler --log-count 100
+ astro deployment logs <deployment-id> --dag-processor --keyword "ImportError"
+ astro deployment logs <deployment-id> --component scheduler --log-count 50 # CLI 1.45+
+ astro deployment logs <deployment-id> --apiserver --log-count 50
+ astro deployment logs <deployment-id> --triggerer --error
+ astro deployment logs <deployment-id> --workers --keyword "OOMKilled"
```
- ### List + inspect DAG runs
+ Use these when:
+ - DAGs not appearing / parse errors → `--dag-processor --keyword "ImportError"` (and `--scheduler` on older runtimes)
+ - Triggerer crashing → `--triggerer --error`
+ - Worker OOM → `--workers --keyword "OOMKilled"`
+
+ ### Environment variables and deploy state
+
```bash
- # Recent runs across all DAGs (Airflow 3.x)
- af "/api/v2/dags/~/dagRuns?limit=20&order_by=-start_date" | jq '.dag_runs[] | {dag_id, run_id, state, start_date}'
+ astro deployment variable list --deployment-id <id> # values redacted
+ astro deployment variable list --deployment-id <id> -s # secrets (sensitive)
+ astro deployment inspect <id>
+ astro deployment pool list --deployment-id <id>
+ ```
- # Runs for a specific DAG
- af "/api/v2/dags/<dag_id>/dagRuns?limit=10&order_by=-start_date" | jq
+ Create/update/copy variables only when the user explicitly asks. Confirm the deployment ID first; Astronomer has no undo.
- # Only failed runs in a date window
- af "/api/v2/dags/<dag_id>/dagRuns?state=failed&start_date_gte=2026-05-01T00:00:00Z" | jq
+ ## DAG-level: prefer `af`
- # Single run detail
- af "/api/v2/dags/<dag_id>/dagRuns/<run_id>" | jq
+ Once an instance points at the target deployment:
+
+ ```bash
+ af health
+ af dags errors
+ af dags list
+ af runs list --dag-id <dag_id>
+ af runs diagnose <dag_id> <run_id>
+ af tasks logs <dag_id> <run_id> <task_id>
+ af tasks logs <dag_id> <run_id> <task_id> --try 2
+ af config pools
+ af api ls --filter xcom
```
- ### Task instances for a run
+ Mutations (`af dags unpause`, `af runs trigger`, `af runs clear`, `af runs delete`) are **opt-in** - only when the user explicitly asks. Default is read-only.
- ```bash
- # All task instances in a run
- af "/api/v2/dags/<dag_id>/dagRuns/<run_id>/taskInstances" \
- | jq '.task_instances[] | {task_id, state, try_number, duration, start_date}'
+ Full command map: [references/af.md](references/af.md).
- # Only the failed ones
- af "/api/v2/dags/<dag_id>/dagRuns/<run_id>/taskInstances?state=failed" | jq
+ ## Curl fallback (Airflow REST)
- # Specific task instance
- af "/api/v2/dags/<dag_id>/dagRuns/<run_id>/taskInstances/<task_id>" | jq
+ Use when `af` is missing or has no remote instance. **Airflow 3.x = `/api/v2/`**, **Airflow 2.x = `/api/v1/`**.
+
+ ```bash
+ export ASTRO_TOKEN="$(gopass show -o <path/to/deployment-token>)"
+ export AF_URL="https://<org>.astronomer.run/<deployment-short-id>"
+ afcurl() { curl -fsSL -H "Authorization: Bearer ${ASTRO_TOKEN}" "${AF_URL}$1"; }
```
- ### Task instance log (Airflow 3.x: structured events, NOT a string)
+ Do not name the wrapper `af()` - that shadows the real CLI.
- **Gotcha (verified June 2026, Airflow 3.1):** the log endpoint returns
- `{"content": [ {event, timestamp, sources, ...}, ... ], "continuation_token": "..."}`.
- `content` is a **list of event objects**, not a string - `jq -r '.content'` yields `null`/nothing
- and looks like an "empty log". Iterate the list and read `.event`. (Only Airflow 2.x `/api/v1/`
- returned `.content` as a plain string.) **URL-encode the run_id** - scheduled IDs contain `+`/`:`
- (`scheduled__2026-06-24T10:00:00+00:00`).
+ Any Astro API token works as `Authorization: Bearer` (Deployment preferred, then Workspace, then Organization). The same value works for the `astro` CLI via `ASTRO_API_TOKEN`.
+ ### Runs and tasks
+
+ **URL-encode `run_id`** - scheduled IDs contain `+` / `:`.
+
```bash
- # Airflow 3.x: extract the real log lines (skip ::group:: UI fold markers)
- af "/api/v2/dags/<dag_id>/dagRuns/<run_id>/taskInstances/<task_id>/logs/<try_number>?full_content=true" \
- | jq -r '.content[] | select(type=="object") | .event' | grep -v '^::' | tail -n 200
+ afcurl "/api/v2/dags/~/dagRuns?limit=20&order_by=-start_date" \
+ | jq '.dag_runs[] | {dag_id, run_id, state, start_date}'
- # Stream a long/running task: replay continuation_token as ?token=<...> until it stops advancing
- af ".../logs/<try>?full_content=true" | jq -r '.continuation_token' # pass back as &token=
+ afcurl "/api/v2/dags/<dag_id>/dagRuns?limit=10&order_by=-start_date" | jq
+ afcurl "/api/v2/dags/<dag_id>/dagRuns?state=failed&start_date_gte=2026-05-01T00:00:00Z" | jq
+ afcurl "/api/v2/dags/<dag_id>/dagRuns/${RUN_ID}/taskInstances?state=failed" | jq
```
- Running tasks DO return logs mid-run. `full_content=true` returns the full first block; without it
- you only get the first chunk. For a long, slow task (e.g. a big backfill), polling Snowflake/state
- for progress is cheaper than streaming the whole log.
+ ### Task logs (Airflow 3.x: `content` is events, not a string)
- ### Other useful read endpoints
+ Verified on Airflow 3.1-3.3: the log endpoint returns
+ `{"content":[{event, timestamp, sources, ...}, ...], "continuation_token":"..."}`.
+ `jq -r '.content'` prints nothing useful. Iterate `.content[] | .event`.
```bash
- af "/api/v2/dags?limit=50&only_active=true" # list DAGs
- af "/api/v2/dags/<dag_id>" # DAG detail
- af "/api/v2/dags/<dag_id>/details" # parsed DAG (schedule, tasks, etc.)
- af "/api/v2/importErrors" # DAG parse errors
- af "/api/v2/connections" # list connections (no secrets)
- af "/api/v2/variables" # list Airflow Variables (values included - careful)
- af "/api/v2/pools" # slot pool status (find queue starvation)
- af "/api/v2/monitor/health" # scheduler / metadata DB health
+ afcurl "/api/v2/dags/<dag_id>/dagRuns/<run_id>/taskInstances/<task_id>/logs/<try_number>?full_content=true" \
+ | jq -r '.content[] | select(type=="object") | .event' | grep -v '^::' | tail -n 200
```
- ### Mutations (opt-in - only when the user explicitly asks; skill is read-only by default)
+ `full_content=true` returns the first full block. Replay `continuation_token` as `?token=` until it stops advancing. Running tasks do return logs mid-run.
- The `af()` wrapper is GET-only; use a write wrapper. Airflow 3.x removed the `dags set-run-state` CLI,
- so terminate runs via the API.
+ Airflow 2.x `/api/v1/` still returns `.content` as a plain string.
+ ### Other reads
+
```bash
+ afcurl "/api/v2/dags?limit=50&only_active=true"
+ afcurl "/api/v2/dags/<dag_id>/details"
+ afcurl "/api/v2/importErrors"
+ afcurl "/api/v2/connections"
+ afcurl "/api/v2/variables" # values included - do not paste secrets
+ afcurl "/api/v2/pools"
+ afcurl "/api/v2/monitor/health"
+ ```
+
+ ### Mutations (opt-in)
+
+ ```bash
afw() { curl -fsSL -X "$1" -H "Authorization: Bearer ${ASTRO_TOKEN}" -H "Content-Type: application/json" "${AF_URL}$2" -d "$3"; }
- afw PATCH "/api/v2/dags/<dag_id>?update_mask=is_paused" '{"is_paused": false}' # unpause (needed before a new DAG's first trigger)
- afw POST "/api/v2/dags/<dag_id>/dagRuns" '{"dag_run_id":"manual__e2e","logical_date":null}' # trigger now
- afw PATCH "/api/v2/dags/<dag_id>/dagRuns/<run_id>" '{"state":"failed"}' # terminate/clear a run (URL-encode run_id)
+ afw PATCH "/api/v2/dags/<dag_id>?update_mask=is_paused" '{"is_paused": false}'
+ afw POST "/api/v2/dags/<dag_id>/dagRuns" '{"dag_run_id":"manual__e2e","logical_date":null}'
+ afw PATCH "/api/v2/dags/<dag_id>/dagRuns/<run_id>" '{"state":"failed"}'
```
- **`max_active_runs=1` gotcha:** unpausing a DAG can spawn one scheduled run for the latest interval
- (catchup=false → just one), so a manual trigger then sits **queued** behind it - two runs in the UI
- ≠ two running concurrently. To run only one, terminate the redundant (preferably the *queued* one, so
- no in-flight work is lost).
+ **`max_active_runs=1`:** unpausing can spawn a scheduled run, so a manual trigger sits queued behind it. Terminate the redundant queued run if the user wants only one.
## Decision tree
```
User wants... → Use
─────────────────────────────────────────────────────────────────────
- "why did this run fail" → REST: dagRuns → taskInstances?state=failed → logs/<try>
- "any failed DAGs today" → REST: /dagRuns?state=failed&start_date_gte=...
- "scheduler is broken / DAGs not parsing" → astro CLI: deployment logs --scheduler
- "task log for try 2 of X" → REST: /taskInstances/X/logs/2
- "worker OOM" → astro CLI: deployment logs --workers --keyword OOMKilled
- "what env vars are set" → astro CLI: deployment variable list
- "copy staging vars to prod" → astro CLI: deployment variable copy
- "DAG won't import" → REST: /importErrors AND astro logs --scheduler
- "compare schedule/tasks staging vs prod" → REST: /dags/<id>/details on both URLs
- "pool is starved" → REST: /pools
- "trigger a backfill" / "clear failed" → DO NOT - read-only token. Ask user to use Astro UI or `make airflow`.
+ "use Otto" / AF2→3 upgrade / long audit → vd:delegating-to-otto
+ "why did this run fail" → af runs diagnose (else curl dagRuns → failed TIs → logs)
+ "any failed DAGs today" → af runs list / curl /dagRuns?state=failed
+ "scheduler broken / DAGs not parsing" → astro logs --dag-processor AND af dags errors
+ "task log for try 2 of X" → af tasks logs ... --try 2
+ "worker OOM" → astro logs --workers --keyword OOMKilled
+ "what env vars are set" → astro deployment variable list
+ "pool is starved" → af config pools
+ "trigger / clear failed" → only if user asks; prefer af, else curl. Never with a read-only token.
```
- ## Patterns Claude should use
-
- ### Investigate "DAG X failed" end-to-end
+ ## Investigate "DAG X failed"
```bash
- export ASTRO_TOKEN="$(gopass show -o <path/to/staging-token>)"
- export AF_URL="https://<org>.astronomer.run/<deployment-short-id>"
- af() { curl -fsSL -H "Authorization: Bearer ${ASTRO_TOKEN}" "${AF_URL}$1"; }
-
- # 1. Get most recent failed run
- RUN_ID=$(af "/api/v2/dags/<dag_id>/dagRuns?state=failed&limit=1&order_by=-start_date" \
+ # 1. most recent failed run
+ af runs list --dag-id <dag_id>
+ # fallback:
+ RUN_ID=$(afcurl "/api/v2/dags/<dag_id>/dagRuns?state=failed&limit=1&order_by=-start_date" \
| jq -r '.dag_runs[0].run_id')
- # 2. Find failed task(s)
- af "/api/v2/dags/<dag_id>/dagRuns/${RUN_ID}/taskInstances?state=failed" \
+ # 2. diagnose (af) or list failed tasks (curl)
+ af runs diagnose <dag_id> "$RUN_ID"
+ afcurl "/api/v2/dags/<dag_id>/dagRuns/${RUN_ID}/taskInstances?state=failed" \
| jq '.task_instances[] | {task_id, try_number}'
- # 3. Pull the log for the failed task's last try (substitute TASK_ID and TRY from step 2)
- af "/api/v2/dags/<dag_id>/dagRuns/${RUN_ID}/taskInstances/<task_id>/logs/<try>" \
- | jq -r '.content' | tail -n 200
- ```
-
- ### Compare staging vs prod env vars
-
- ```bash
- diff \
- <(astro deployment variable list --deployment-id <staging-id> -s | sort) \
- <(astro deployment variable list --deployment-id <prod-id> -s | sort)
- ```
-
- ### Tail scheduler logs for parse errors after a deploy
-
- ```bash
- astro deployment logs <id> --scheduler --log-count 200 \
- --keyword "ImportError\|SyntaxError\|Broken DAG"
+ # 3. logs - NEVER jq -r '.content' on Airflow 3
+ af tasks logs <dag_id> "$RUN_ID" <task_id>
+ afcurl "/api/v2/dags/<dag_id>/dagRuns/${RUN_ID}/taskInstances/<task_id>/logs/<try>?full_content=true" \
+ | jq -r '.content[] | select(type=="object") | .event' | grep -v '^::' | tail -n 200
```
## Safety rules
- - **Read-only by default.** This skill never triggers DAGs, clears tasks, or mutates state via API. If the user needs that, point them to the Astro UI or local `make airflow CMD='dags trigger ...'`.
- - **Token discipline.** Pull from gopass. Never echo `$ASTRO_TOKEN` to stdout. Never write it to a file outside the gopass store. Never commit to git.
- - **Distinct tokens per environment.** Don't reuse one token for staging + prod - different blast radii.
- - **`--keyword` is regex, not glob.** Escape special chars (`.`, `(`, `|`).
- - **Log fetch is heavy.** Task logs on Astro can be multi-MB (Kubernetes pod logs in S3). Always tail with `| tail -n 200` unless explicitly asked for full log.
- - **Don't paste secret values into chat** when listing variables - summarize names only.
- - **Confirm deployment ID before mutating** any env var. Astronomer offers no undo for `variable update`.
+ - **Read-only by default.** Do not trigger, clear, pause, or update variables unless the user asks.
+ - **Token discipline.** Pull from gopass. Never echo `$ASTRO_TOKEN`. Never write it outside the password store. Never commit it.
+ - **Distinct tokens per environment.**
+ - **`--keyword` is an exact phrase.** Do not pass `foo|bar` regex.
+ - **Log fetch is heavy.** Tail with `| tail -n 200` unless asked for the full log.
+ - **Do not paste secret variable values into chat.**
+ - **Confirm deployment ID** (`astro deployment list`) before any mutation.
## Failure modes
| Symptom | Cause | Fix |
|---|---|---|
| `401 Unauthorized` | token expired or wrong scope | Re-mint in Astro UI, update gopass |
- | `403 Forbidden` on POST endpoints | token uses `WORKSPACE_MEMBER` role (POST blocked by auth proxy) | Use `DEPLOYMENT_ADMIN` or a custom role with required perms |
- | `404` on `/api/v2/...` | deployment runs Airflow 2.x, not 3.x | Try `/api/v1/...` |
- | `astro deployment logs` flag error `unknown flag: --component` | wrong CLI syntax | Use boolean flags: `--scheduler`, `--apiserver`, `--triggerer`, `--workers`, `--webserver` |
- | `cannot query for more than one log level and/or keyword at a time` | combining `--error` + `--keyword` | Run one, grep the output for the other |
- | `astro deployment logs` returns nothing | wrong component flag for Airflow version | Airflow 3.x uses `--apiserver`, not `--webserver` |
- | Empty `dag_runs` array | DAG never ran, or `start_date_gte` filter too narrow | Drop the filter, check `paused` state on the DAG |
- | Log fetch returns truncated content | response paginated via `continuation_token` | Loop with `?token=<continuation_token>` until empty |
- | `astro` CLI returns "context not found" | not logged into the right org | `astro context list && astro context switch <name>` |
+ | `403 Forbidden` on POST | token role is `WORKSPACE_MEMBER` (POST blocked) | `DEPLOYMENT_ADMIN` or a custom role with the write perm |
+ | `404` on `/api/v2/...` | Airflow 2.x | Use `/api/v1/` |
+ | `astro deployment logs` prints Flags/Usage and exits | combined `--error`+`--keyword` or two level flags; or bad deployment ID | One filter only; re-run `astro deployment list` |
+ | `unknown flag: --component` | Astro CLI < 1.45 | Upgrade, or use `--scheduler` / `--dag-processor` / `--apiserver` |
+ | `--error` returns `[info]` lines | text contains "error" (DAG id, message) | Use `--keyword` on `--dag-processor` instead |
+ | `No matching logs` with `foo\|bar` | `--keyword` is exact phrase, not regex | Search one literal at a time |
+ | Empty `dag_runs` | never ran, or date filter too tight | Drop the filter; check `is_paused` |
+ | Log body looks empty | `jq -r '.content'` on Airflow 3 events | Use `.content[] \| .event` |
+ | Truncated logs | `continuation_token` | Loop `?token=` until unchanged |
+ | `af: command not found` | CLI not installed | `uvx --from astro-airflow-mcp af` |
+ | `af` only shows localhost | no remote instance | `instance discover --dry-run` then ask; or `instance add` |
+ | `context not found` | wrong org | `astro context list && astro context switch <name>` |
## Discovery
```bash
- astro context list # which org/workspace am I in
- astro deployment list # available deployments
- astro version # CLI version (some flags need ≥ 1.30)
- astro deployment inspect <id> | jq # everything about a deployment
-
- # Find an Airflow API endpoint surface
- af "/api/v2/" # 3.x root
- af "/api/v1/" # 2.x root
+ astro version
+ astro context list
+ astro deployment list
+ astro deployment inspect <id> --key metadata.airflow_api_url
+ uvx --from astro-airflow-mcp af instance list
```
## References
+ - `af` command map: [references/af.md](references/af.md)
+ - Official Airflow ops skill (af-centric): https://github.com/astronomer/agents/blob/main/skills/airflow/SKILL.md
- Astro CLI: https://docs.astronomer.io/astro/cli/overview
- - Astro CLI command reference: https://docs.astronomer.io/astro/cli/reference
- - Astro Deployment API tokens: https://docs.astronomer.io/astro/deployment-api-tokens
- - Custom Deployment roles: https://docs.astronomer.io/astro/customize-deployment-roles
- - Airflow 3.x REST API: https://airflow.apache.org/docs/apache-airflow/stable/stable-rest-api-ref.html
- - For a structured MCP alternative (adds a plugin to the deployment image, more setup), see Astronomer's `astro-airflow-mcp` package.
+ - Airflow 3 REST API: https://airflow.apache.org/docs/apache-airflow/stable/stable-rest-api-ref.html
+ - Otto delegation: `vd:delegating-to-otto`