ADR 0038: Connector dependency ergonomics β connectors: sugar + dependencies: escape hatch¶
Status: Accepted Date: 2026-06-03 Companions: ADR 0036 (Airflow runtime compat shim β deferred; the task image ships the real Airflow Task SDK), ADR 0024 (parser structural shim), ADR 0014 (admin panel is Go-only)
Context¶
With ADR 0036's runtime shim deferred, the task image ships the real
apache-airflow-task-sdk (it pulls apache-airflow-core), under which provider
hooks already work β the env-secrets backend reads AIRFLOW_CONN_*. To use an
Airflow connector, a user's DAG does:
from airflow.providers.postgres.hooks.postgres import PostgresHook
hook = PostgresHook(postgres_conn_id="pg_target")
hook.get_records("SELECT 1")
β¦and declares the provider in leoflow.yaml. Because the install is normal
(not --no-deps), the provider pulls its own driver, so the minimum is one line:
Two frictions remain: (1) the user must know the exact package name
(apache-airflow-providers-postgres, and that amazon.aws β β¦-amazon but
microsoft.mssql β β¦-microsoft-mssql), and (2) forgetting it yields a cryptic
ModuleNotFoundError at task runtime, after the DAG was already dispatched.
Leoflow already maintains a curated connection-type catalog
(internal/api/connection_hook_meta.go β conn_type β hook class), so the
mapping conn_type β pip package is a small, already-half-built table.
Decision¶
- Sugar field
connectors:inleoflow.yaml. Short connection-type names expand, at compile, into provider packages via the curated catalog:
# the easy path β for the common connectors
connectors:
- postgres # β apache-airflow-providers-postgres (driver auto-pulled)
- http
The expansion is logged (postgres β apache-airflow-providers-postgres) so it
is never opaque.
dependencies:stays as the escape hatch β for advanced users. Arbitrary packages, pinned versions, and any connector not in the catalog:
dependencies:
- apache-airflow-providers-postgres>=6.0,<7 # pin
- apache-airflow-providers-someobscure # not in the catalog
- duckdb==1.1.3 # any lib
The user picks whichever style they prefer; the two coexist. Effective install
set = expand(connectors) βͺ dependencies.
-
The catalog is the single source of truth (
conn_type β pip package), shared by three consumers: theconnectors:expansion, the admin-panel / connection-probe nudge, and the compile-time validation below. -
Clear, actionable errors at compile (not at runtime):
- An unknown value in
connectors:fails compile: "unknown connector 'postgres2'; known: postgres, mysql, β¦; or add the pip package todependencies:." - A
dag.pythat importsfrom airflow.providers.<X>.hooksβ¦whose provider is in neitherconnectors:nordependencies:fails compile with the exact line to add: "PostgresHook needsapache-airflow-providers-postgresβ add it toconnectors: [postgres]ordependencies:."
Consequences¶
- Better UX, lower error rate. The common case is
connectors: [postgres]; the tool maps the package and the driver comes free. The advanced case keeps full control (pinning, arbitrary packages). Missing deps fail loud and early (compile) with the fix in the message, not as a cryptic runtime import error. - A product differentiator. "Declare
connectors: [postgres]and you're done" is a cleaner story than Airflow's "find the right provider package and driver yourself." - Maintenance: the catalog must track Airflow's provider package names β but
it is already maintained for the admin form (15 common types); anything outside
it falls back to
dependencies:, so coverage is never a hard wall. - No version pinning in the sugar (by design β
connectors: [postgres]carries no version). Users who must pin usedependencies:. A futurepostgres@6.0shorthand can be added without breaking the bare form. - Cross-cutting source of truth: the catalog now carries the pip package, so the parser/compiler (Go + Python) and the API all read one place; drift between the form, the sugar, and the validator is structurally prevented.