ADR 0001: Why Leoflow and Not Apache Airflow's KubernetesExecutor
ADR 0001: Why Leoflow and Not Apache Airflow’s KubernetesExecutor
Version v0.5.5 of the documentation is no longer actively maintained. The site that you are currently viewing is an archived snapshot. For up-to-date documentation, see the latest version.
The why behind Leoflow’s design. ADRs are immutable once accepted. To record a new decision, start from the template at the end of this page.
leoflow.yaml Abstraction Layerv0.0.1connectors: sugar + dependencies: escape hatchleoflow deploy — pipeline-less promotion from Lite to ProAn ADR records one decision and why it was made; what a large change will build goes in a spec (ADR 0069).
File it as NNNN-short-title.md in this directory, with the next free
number: check open pull requests as well as main, because two drafts can
take the same number. Open it as its own pull request, which also adds it to
the list above.
Status starts as Proposed and becomes Accepted when the decision is
taken. From then on the record is not rewritten: a later decision gets a new
ADR that names the one it replaces in its Relates line.
---
title: "ADR NNNN: <the decision, as a sentence>"
linkTitle: "NNNN · <short title>"
weight: NNNN0
description: "ADR NNNN: <one sentence>"
---
**Status:** Proposed
**Date:** YYYY-MM-DD
**Relates:** ADR <n> (<how it relates>), ...
## Context
The problem and the forces at play, with evidence: issues, incidents,
measurements, the code as it is today.
## Decision
What is decided, stated so a reviewer can check a change against it.
## Alternatives considered
Each option that was rejected, and why.
## Consequences
What becomes easier, what becomes harder, and what follows from it
(specs, migrations, settings, docs).
ADR 0001: Why Leoflow and Not Apache Airflow’s KubernetesExecutor
ADR 0002: Pod-per-Task Execution Model
ADR 0003: DAG-as-Image with leoflow.yaml Abstraction Layer
ADR 0004: Thin Static Go Agent in the Worker Container
ADR 0005: Hybrid DAG Authoring with Compile-Time Parsing
ADR 0006: XCom with Redis Backend
ADR 0007: Airflow UI Compatibility for the MVP
ADR 0008: JWT Authentication with OIDC-Ready Interface
ADR 0009: Leader Election via Postgres Advisory Locks
ADR 0010: Observability Stack from Day One
ADR 0011: Test-Driven Development (Strict)
ADR 0012: Code Quality Standards (Go Report Card A+ as Floor)
ADR 0013: API Documentation via Scalar, Embedded in the Server Binary
ADR 0014: Supply Chain Security Stack
ADR 0015: Kubernetes as the Sole Container Execution Path (No Docker SDK)
ADR 0016: Deferrable Tasks (Deferred to v0.3)
ADR 0017: UI Static Asset Serving Strategy
ADR 0018: UI Custom as Strategic North Star
ADR 0019: Secret Encryption at Rest (Connections)
ADR 0020: “Delete DAG” Clears History; Deregister Is Separate
ADR 0021: Exposing Variables and Connections to Task Pods
ADR 0022: Ephemeral Per-DAG-Run Staging Volume
ADR 0023: DAG Authoring — Config Binding and Override Layers
ADR 0024: DAG Parsing via a Structural Shim (No Airflow SDK Dependency)
ADR 0025: Embedded Monaco Web Editor for Leoflow Lite
ADR 0026: Lite Datastore — XCom on Postgres, No Redis
ADR 0027: Product Editions — Executors and Delivery
ADR 0028: Release & Versioning for the Two Editions (One Tag, Two Co-Versioned Artifacts)
ADR 0029: Lite Datastore Default — Docker Postgres (Managed PG is the Opt-In)
ADR 0030: Lite Datastore Auto-Selects — Docker Postgres, or a Managed PG When Docker Is Absent
ADR 0031: Scheduler Architecture — Reconciliation Loop, Two-Phase Dispatch, Two-Layer Reaping
ADR 0032: Task Return Values Are Not Logged — Only Their Metadata Is
ADR 0033: Release Flow — RC Tags, E2E Gates, and Immutable Versions
ADR 0034: Fan-in / map-reduce — list-of-upstream parameter binding
ADR 0035: Cloud connector auth — keyless-first; Leoflow is not a key manager
ADR 0036: Airflow 3.X runtime compatibility shim — one model, one policy seam
ADR 0037: Release version scheme — skip alpha/beta, RC discipline from v0.0.1
ADR 0038: Connector dependency ergonomics — connectors: sugar + dependencies: escape hatch
ADR 0039: Generated connector catalog with full form fidelity
ADR 0040: Airflow operator + sensor execution — native fast path + generic executor
ADR 0041: leoflow deploy — pipeline-less promotion from Lite to Pro
ADR 0042: dbt support via native-Go manifest rendering
ADR 0043: TaskGroup as a first-class construct with split/fused execution
ADR 0044: dbt multi-project — one project per business domain
ADR 0045: Secrets reach a task because it declared them
ADR 0046: Coverage — one rule, per package, counting the tests we already wrote
ADR 0047: Deprecate the native inline http_api; run HTTP through the generic pod executor
ADR 0048: The control plane executes no user-influenced code or network requests
ADR 0049: Split the API/UI and scheduler into separate roles of one binary
ADR 0050: Model Context Protocol (MCP) server
ADR 0051: Separate the orchestration and execution state machines
ADR 0052: Durable task outcome — decouple the task result from report delivery
ADR 0053: Admission + placement — one scheduler-side layer for task concurrency and pod assignment
ADR 0054: Coexistence in a shared, multi-team Kubernetes cluster
ADR 0055: Secret scoping and token liveness — scope by declaration, exchange the token, bind it to task liveness
ADR 0056: Task-log object sink — native dual-SDK (S3 + GCS), keyless-first
ADR 0057: OIDC/SSO authentication with fail-closed tenant pinning
ADR 0058: Warm worker pools — pod-reuse semantics (N:1)
ADR 0059: OpenLineage emission from the Go control plane → OpenMetadata
ADR 0060: External secrets resolution — a pod-side, provider-neutral SecretResolver (AWS reference)
ADR 0061: Secret material is written only to a private, ephemeral scratch (never the project/CWD/repo) and masked on read — the invariant that prevents the credential-leak class.
ADR 0062: main stays open for every change; each minor gets a release-X.Y branch that tags its patches; fixes land on main first and are cherry-picked after review.
ADR 0065: An explicit, operator-triggered dexaflow lite migrate-key that records every key before touching a row, re-encrypts in one verified transaction, and drops the predecessor only after a clean pass.
ADR 0068: while Dexaflow is 0.x, a patch may carry features and migrations when they meet a fixed safety bar; reviews check the bar instead of asking for a minor.
ADR 0069: what a contributor needs to change Dexaflow correctly lives in the repository as plain Markdown (AGENTS.md, nested AGENTS.md, recipes, specs); files that configure one editor or assistant stay out of it.
ADR 0070: a secret reaches task code as an environment variable (or a Secret the kubelet resolves), never as text rendered into a command line; templated bash keeps its Airflow parity for non-secret values only, and a lint becomes a policy once multi-tenancy needs it.
ADR 0071: a release tag publishes only immutable, versioned artifacts and a GitHub release that is not Latest; one promote job, after every gate is green, pushes the Helm chart, marks Latest (GA of the newest minor only) and moves the py3.N runtime tags.
ADR 0072: amends ADR 0014’s CodeQL policy. A dedicated job fails a pull request that introduces an open CodeQL alert of high or critical security severity; alerts already open on the base branch warn; only checks the rulesets require block a merge.
ADR 0073: the CLI resolves a builder in a fixed order (skip, code layer, daemonless dependency layer, remote builder, local daemon), talks to registries through a pure Go client (go-containerregistry, never the Docker SDK), and addresses dependency images by the hash of what they contain.
ADR 0074: long-lived API tokens owned by a user, stored as a SHA-256 hash, narrowed by the ADR 0067 scopes, expiring and revocable; a deploy:dag permission and a built-in deployer role that registers versions and rolls back, and cannot delete.
ADR 0075: replaces section 6 of ADR 0073. The registry client, the skip and code-layer paths, the daemonless dependency layer and the remote builder ship in 0.5.4, each behind the builder seam. From the registry client on, the digest a deploy registers and the platform check of a multi-platform push are read from the registry, not from a local image store.
ADR 0076: the first form of the skip path of ADR 0073. When the image tag is the default content label and the registry already holds an image under it for every requested platform, deploy pins and registers that image without building or pushing. Records what the label does not cover and who decides what a reused image contains.
ADR 0077: the code-layer path of ADR 0073. A pushed build of the generated Dockerfile becomes a dependency image, reused by its key, plus one deterministic layer with the DAG code appended through the registry. Until the daemonless dependency layer lands, the local daemon builds the dependency image, and only projects whose Python dependencies are pinned take the path.
ADR 0078: the dependency-layer path of ADR 0073. When the dependency image of a split build is missing and uv is on PATH, the CLI resolves the Python dependencies for each target platform with uv, installs the wheels on the host, packs them as one deterministic layer on the runtime base pinned by digest and pushes the dependency image through the registry API. The key covers the resolved set, so unpinned dependencies take the split too.
ADR 0079: dexaflow auth login signs in with the cluster’s OIDC provider by driving the server’s own sign-in in a browser. After a consent click, the server hands the CLI a one-time code on a loopback redirect, bound to a PKCE challenge, and the CLI exchanges it for the token a password login returns. The CLI never talks to the IdP.
ADR 0080: the CLI’s registry client reads a credential from the docker config and its credential helpers first, then, for an Artifact Registry, GCR, ECR or ACR host, from the token the gcloud, aws or az CLI prints. No cloud SDK is linked. registry.auth_method chooses between them: auto (the new default), docker_config and cloud; env and iam still load, deprecated.
ADR 0081: dexaflow auth login –device follows the shape of the device authorization grant (RFC 8628), served by Dexaflow rather than by the IdP. The person opens a Dexaflow page, signs in through the server’s own OIDC flow, then enters the short code and confirms it before approving; the CLI polls the server. Pending sign-ins live in the database so any replica can answer.
ADR 0082: with warm pools on, the reconciler keeps a pool for the current version of every unpaused DAG that declares min_idle_workers from the moment it becomes current, and for an active run’s version only while it is still current. A superseded version keeps no idle buffer: its idle workers are deleted and busy ones finish their attempt first. Replaces the version bump paragraph of ADR 0058 D1.
ADR 0083: the remote path of ADR 0073. When a build needs a real Dockerfile build (a package without wheels, system_packages, a project Dockerfile) and build.remote_builder (or the target’s or workspace’s remote_builder) names a Cloud Build project, the CLI packs the build context itself with the .dockerignore applied, submits it with gcloud builds submit –async, follows it, lets Cloud Build push, and registers the digest the registry holds once it matches the one the build reports. No cloud SDK is linked. The in-cluster build pod is the next backend behind the same seam.
ADR 0084: when a task’s Jinja templates cannot render, on a captured operator’s template_fields or on a templated bash command, the attempt fails before execute() or bash runs, with the field name and the Jinja error in the task log, as on Airflow. The run context gains macros and logical_date in the same change. Replaces the best-effort templating of ADR 0040 Phase A.
ADR 0085: the executor’s Role gains one read verb, get on serviceaccounts in the task namespace, so the register dry run can tell a deploy, before the build, that the ServiceAccount a task pod would run as does not exist or carries no pull secret. The grant renders only in the all role; the api role of a split control plane stays without cluster access (ADR 0049) and reports the accounts unchecked.
ADR 0086: run_on_latest_version moves a run to the DAG’s current version only when the run has finished and none of its task instances is queued or running, as in Airflow 3.2. The same transaction creates a task instance for every task the version adds, marks removed the task instances of the tasks it dropped (a new terminal state) and brings back a removed one whose task returns. A run that has not finished keeps its version, the clear applies on it, and the response says the option was not applied and why. Replaces the refusal added by #1876.
ADR 0087: replaces point 2 of ADR 0075 for the local daemon. docker pushes with buildx and a metadata file, podman through a manifest list and a digest file, nerdctl with all its platforms; the digest a deploy registers is the one the tool reports for its push, and the registry is read at that digest only to check a multi-platform push.
ADR 0088: dexaflow.yaml declares build args, build secrets read from the environment by reference, and a package index that replaces PyPI. Secrets and the index URL reach the build as BuildKit secrets or uv’s environment, never a command line, a layer or dag.json; a build that has them runs on the local machine, never on a remote builder.
ADR 0089: a warm pool is keyed by tenant, DAG, the dependency image digest and a hash of the warm pod’s runtime settings instead of by dag_version_id. A version built on the code-layer path delivers its code to the worker per attempt as a bundle verified by digest before import, so a deploy that changes only code keeps the pool warm. Replaces the pool key of ADR 0058 D1 for those versions; every other version keeps its per-version pool.
ADR 0091: dexaflow registry login signs in to a registry no cloud owns without docker. For GHCR it exchanges the gh CLI session; otherwise it takes a token on stdin. The credential is checked with the registry and stored in the docker config or its credential helper, where the registry client and every container tool read it.
ADR 0092: the control plane trusts a list of CI OIDC issuers (GitHub Actions, GitLab, Google Cloud Build), each with its audience and the one tenant it deploys to; ordered rules map claims such as the repository, the ref and the environment to a role; a verified token is exchanged for a short deploy session; dexaflow ci init writes this path when the cluster offers it and the deploy token path otherwise.