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:auditandbadge. Reaudit and history live on the interactive surface only.
Roles are enforced per project on every route:
| Route | Role |
|---|---|
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/audit | Viewer |
GET v1/{projectId}/integrations/airflow/badge | Viewer |
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.