Apache Airflow logoAPI

The Airflow Integration API, End to End: Every Route, Role, and Payload

Every vendor integration in CoreModels answers the same first question the same way. Ask the platform what it knows how to govern:

The Airflow Integration API, End to End: Every Route, Role, and Payload

Every vendor integration in CoreModels answers the same first question the same way. Ask the platform what it knows how to govern:

GET https://coremodels.example.com/graph/integrations/vendors
Authorization: Bearer $TOKEN
{
  "success": true,
  "vendors": [
    {
      "key": "airflow",
      "displayName": "Apache Airflow",
      "capabilities": "Import, Audit",
      "artifacts": {
        "dags": "required — GET /api/v1/dags (the full response or just its 'dags' array)",
        "tasks": "optional — {\"<dag_id>\": <GET /api/v1/dags/{dag_id}/tasks response>} aggregated per DAG",
        "datasets": "optional — GET /api/v1/datasets (data-aware scheduling assets; becomes cross-DAG lineage)"
      }
    }
  ]
}

That one response (the vendors array lists every registered connector; abridged here to the Airflow entry) is the contract for this article: the vendor key is airflow, the artifact names and their sources are self-describing, and the capability string tells you up front that this connector imports and audits but does not generate. This piece walks the HTTP surface of that vendor's import-audit loop — both surfaces, every verb of the loop, real payloads, and the role each call requires.

Two surfaces, one distinction

The Airflow integration lives on two HTTP surfaces:

  • Interactive surface — routes under graph/integrations/..., authenticated with your normal CoreModels login token. This carries the import-audit loop's full verb set: import, audit, reaudit, history, badge, generate, status.
  • Machine-to-machine surface — routes under v1/..., where user API keys work. This is what CI should call. It deliberately carries only two verbs: audit and badge. Reaudit and history live on the interactive surface only.

Roles are enforced per project on every route:

RouteRole
POST graph/integrations/airflow/import/{projectId}Admin
POST graph/integrations/airflow/audit/{projectId}Viewer
POST graph/integrations/airflow/reaudit/{projectId}Viewer
GET graph/integrations/airflow/history/{projectId}Viewer
GET graph/integrations/airflow/badge/{projectId}Viewer
POST graph/integrations/airflow/generate/{projectId}Viewer
GET graph/integrations/airflow/status/{projectId}Viewer
POST v1/{projectId}/integrations/airflow/auditViewer
GET v1/{projectId}/integrations/airflow/badgeViewer

The posture behind that table: import is the only verb that writes to the graph, and it writes additively — existing governed nodes are never mutated. Audit and generate never write anything; recording an audit run into the history is opt-in bookkeeping, and only the reaudit verb always records its run.

Import

POST graph/integrations/airflow/import/{projectId} takes an artifacts request — artifact name to raw content, with the file contents passed as JSON strings:

POST https://coremodels.example.com/graph/integrations/airflow/import/{PROJECT_ID}
Authorization: Bearer $TOKEN
Content-Type: application/json

{ "artifacts": { "dags": "<dags.json>", "tasks": "<tasks.json>", "datasets": "<datasets.json>" } }

The body supports two optional properties beyond artifacts: spaces (target space ids; empty means the project's main space) and recordHistory (audit only — meaningless on import). Send no artifacts at all and you get an explicit refusal: Body must include 'artifacts': { "<name>": "<content>" } (e.g. manifest for dbt). Misspell the vendor and the error names every registered key: Unknown vendor '<v>'. Registered: ....

The response reports datasetsAdded, datasetsSkippedExisting, fieldsAdded, lineageEdgesAdded, lineageEdgesSkipped, nodesEnriched, and snapshotStored, plus lossiness (what was approximated — a success channel, not a failure) and errors (what could not proceed). For Airflow, "datasets" means DAGs and data-aware-scheduling assets, "fields" means tasks, and snapshotStored: true means the parsed deployment was persisted for later re-audits.

Audit

Same body shape, Viewer role, strictly read-only:

POST https://coremodels.example.com/graph/integrations/airflow/audit/{PROJECT_ID}
Authorization: Bearer $TOKEN
Content-Type: application/json

{ "artifacts": { "dags": "<dags.json>", "datasets": "<datasets.json>" }, "recordHistory": true }

The response is the full audit report (abridged here):

{
  "success": true,
  "vendor": "airflow",
  "projectName": "airflow",
  "errorCount": 0,
  "warningCount": 1,
  "infoCount": 1,
  "codes": { "asset-unproduced": 1, "dag-no-description": 1 },
  "driftedObjects": [],
  "fingerprint": "4be09c11a2f37d80",
  "metrics": { "Datasets (estate)": "14", "Datasets governed": "14 / 14", "Fields governed": "48 / 48" },
  "findings": [
    { "section": "Conformance", "severity": "Warning", "code": "asset-unproduced",
      "subject": "s3://lake/clickstream.parquet",
      "message": "Asset is consumed by DAGs but produced by nothing in this deployment — an ungoverned upstream dependency.",
      "detail": null }
  ],
  "markdown": "…",
  "historyRecorded": true,
  "lossiness": []
}

errorCount > 0 is the CI-gate fail signal. codes aggregates finding codes to counts, driftedObjects lists the vendor identities of drifted objects, fingerprint is a content hash of the audited artifacts (useful for correlating history entries), and markdown is the complete human-readable report. Findings fall into three sections — Coverage, Drift, Conformance — with severities Error, Warning, Info. With recordHistory: true, the run lands in the audit trail with trigger audit.

Reaudit

The audit above asks "do these fresh artifacts still conform to the governed model?" Reaudit asks the reverse: "the governed model changed — does the last-known estate still conform?" It runs the same audit engine over the deployment snapshot stored at import time, so it needs no artifacts at all:

POST https://coremodels.example.com/graph/integrations/airflow/reaudit/{PROJECT_ID}
Authorization: Bearer $TOKEN
Content-Type: application/json

{}

The optional body properties are projectName (the vendor-side project name to re-audit; null means the latest stored snapshot), spaces, and notifySlack (post a notification to the project's configured Slack webhook when the recorded run has errors or warnings; default false). The response shape is the same audit report, and the run is always recorded in the history with trigger reaudit — that is the one verb where recording is not opt-in. If the stored snapshot exceeded the ~1.5 MB storage cap at import time (snapshotStored: false), there is nothing to reaudit against; fresh-artifact audits still work.

History

GET graph/integrations/airflow/history/{projectId} returns the rolling trail, newest first, grouped by vendor-side project:

{
  "success": true,
  "vendor": "airflow",
  "projects": [
    {
      "projectName": "airflow",
      "runs": [
        { "at": "2026-07-29T06:41:12.4183920+00:00", "trigger": "reaudit", "errorCount": 1,
          "warningCount": 1, "infoCount": 0, "codes": { "dataset-removed": 1, "dag-no-owner": 1 },
          "fingerprint": "4be09c11a2f37d80" },
        { "at": "2026-07-28T18:02:55.0917744+00:00", "trigger": "audit", "errorCount": 0,
          "warningCount": 1, "infoCount": 1, "codes": { "asset-unproduced": 1, "dag-no-description": 1 },
          "fingerprint": "4be09c11a2f37d80" }
      ]
    }
  ]
}

Each run records when, what triggered it (audit, ci, reaudit, or scheduled), the severity counts, the per-code tallies, and the artifact fingerprint. Same fingerprint but different counts, as above, means the governed model moved, not the estate.

Badge and status

GET graph/integrations/airflow/badge/{projectId} returns image/svg+xml — a shields-style badge rendered from the latest recorded run, labeled airflow audit: green when clean, yellow for warnings only, red for errors, gray when no runs are recorded. GET graph/integrations/airflow/status/{projectId} returns the last-import state:

{ "success": true, "vendor": "airflow", "imported": true, "state": { "…": "…" }, "governedDatasets": 14 }

state carries the bookkeeping recorded at import (timestamps, fingerprint, counts, estate facts); governedDatasets counts vendor-identified datasets that resolve to a governed Type.

Generate: an honest no

The controller exposes POST graph/integrations/{vendor}/generate/{projectId} for every vendor, but the Airflow connector's capabilities are Import and Audit only — so we should be equally plain: there is no Generate section for Airflow beyond this paragraph. A generate call against airflow returns success: false with an explicit error — "Connector 'airflow' does not support generation." — and nothing is silently emitted: the capability gate refuses before any emission code runs. A governed schema can legitimately generate warehouse DDL or dbt contracts; it cannot know your operator logic, and pretending otherwise would be generating fiction.

The v1 surface for machines

CI and other automation should use the API-key surface. The audit is the same engine with the response wrapped in an ApiResponse envelope, so every field moves under data:

POST https://coremodels.example.com/v1/{PROJECT_ID}/integrations/airflow/audit
Authorization: Bearer $TOKEN
Content-Type: application/json

{ "artifacts": { "dags": "<dags.json>" }, "recordHistory": true }

Gate on data.errorCount; render data.markdown into your job summary. Runs recorded from this surface carry trigger ci. And GET v1/{PROJECT_ID}/integrations/airflow/badge serves the same SVG badge on an API key, embeddable in READMEs.

That is the whole import-audit loop: seven interactive verbs plus discovery, two machine verbs, one honest refusal. The self-contained extraction recipe and CI walkthrough live in the Apache Airflow quickstart in the CoreModels integration docs.