Version v0.5.4 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.

Architecture Decision Records

The why behind Leoflow design decisions — one record per decision, immutable once accepted.

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.

Template

An 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 0001: Why Leoflow and Not Apache Airflow’s KubernetesExecutor

ADR 0002: Pod-per-Task Execution Model

ADR 0002: Pod-per-Task Execution Model

ADR 0003: DAG-as-Image with `leoflow.yaml` Abstraction Layer

ADR 0003: DAG-as-Image with leoflow.yaml Abstraction Layer

ADR 0004: Thin Static Go Agent in the Worker Container

ADR 0004: Thin Static Go Agent in the Worker Container

ADR 0005: Hybrid DAG Authoring with Compile-Time Parsing

ADR 0005: Hybrid DAG Authoring with Compile-Time Parsing

ADR 0006: XCom with Redis Backend

ADR 0006: XCom with Redis Backend

ADR 0007: Airflow UI Compatibility for the MVP

ADR 0007: Airflow UI Compatibility for the MVP

ADR 0008: JWT Authentication with OIDC-Ready Interface

ADR 0008: JWT Authentication with OIDC-Ready Interface

ADR 0009: Leader Election via Postgres Advisory Locks

ADR 0009: Leader Election via Postgres Advisory Locks

ADR 0010: Observability Stack from Day One

ADR 0010: Observability Stack from Day One

ADR 0011: Test-Driven Development (Strict)

ADR 0011: Test-Driven Development (Strict)

ADR 0012: Code Quality Standards (Go Report Card A+ as Floor)

ADR 0012: Code Quality Standards (Go Report Card A+ as Floor)

ADR 0013: API Documentation via Scalar, Embedded in the Server Binary

ADR 0013: API Documentation via Scalar, Embedded in the Server Binary

ADR 0014: Supply Chain Security Stack

ADR 0014: Supply Chain Security Stack

ADR 0015: Kubernetes as the Sole Container Execution Path (No Docker SDK)

ADR 0015: Kubernetes as the Sole Container Execution Path (No Docker SDK)

ADR 0016: Deferrable Tasks (Deferred to v0.3)

ADR 0016: Deferrable Tasks (Deferred to v0.3)

ADR 0017: UI Static Asset Serving Strategy

ADR 0017: UI Static Asset Serving Strategy

ADR 0018: UI Custom as Strategic North Star

ADR 0018: UI Custom as Strategic North Star

ADR 0019: Secret Encryption at Rest (Connections)

ADR 0019: Secret Encryption at Rest (Connections)

ADR 0020: "Delete DAG" Clears History; Deregister Is Separate

ADR 0020: “Delete DAG” Clears History; Deregister Is Separate

ADR 0021: Exposing Variables and Connections to Task Pods

ADR 0021: Exposing Variables and Connections to Task Pods

ADR 0022: Ephemeral Per-DAG-Run Staging Volume

ADR 0022: Ephemeral Per-DAG-Run Staging Volume

ADR 0023: DAG Authoring — Config Binding and Override Layers

ADR 0023: DAG Authoring — Config Binding and Override Layers

ADR 0024: DAG Parsing via a Structural Shim (No Airflow SDK Dependency)

ADR 0024: DAG Parsing via a Structural Shim (No Airflow SDK Dependency)

ADR 0025: Embedded Monaco Web Editor for Leoflow Lite

ADR 0025: Embedded Monaco Web Editor for Leoflow Lite

ADR 0026: Lite Datastore — XCom on Postgres, No Redis

ADR 0026: Lite Datastore — XCom on Postgres, No Redis

ADR 0027: Product Editions — Executors and Delivery

ADR 0027: Product Editions — Executors and Delivery

ADR 0028: Release & Versioning for the Two Editions (One Tag, Two Co-Versioned Artifacts)

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 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 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 0031: Scheduler Architecture — Reconciliation Loop, Two-Phase Dispatch, Two-Layer Reaping

ADR 0032: Task Return Values Are Not Logged — Only Their Metadata Is

ADR 0032: Task Return Values Are Not Logged — Only Their Metadata Is

ADR 0033: Release Flow — RC Tags, E2E Gates, and Immutable Versions

ADR 0033: Release Flow — RC Tags, E2E Gates, and Immutable Versions

ADR 0034: Fan-in / map-reduce — list-of-upstream parameter binding

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 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 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 0037: Release version scheme — skip alpha/beta, RC discipline from v0.0.1

ADR 0038: Connector dependency ergonomics — `connectors:` sugar + `dependencies:` escape hatch

ADR 0038: Connector dependency ergonomics — connectors: sugar + dependencies: escape hatch

ADR 0039: Generated connector catalog with full form fidelity

ADR 0039: Generated connector catalog with full form fidelity

ADR 0040: Airflow operator + sensor execution — native fast path + generic executor

ADR 0040: Airflow operator + sensor execution — native fast path + generic executor

ADR 0041: `leoflow deploy` — pipeline-less promotion from Lite to Pro

ADR 0041: leoflow deploy — pipeline-less promotion from Lite to Pro

ADR 0042: dbt support via native-Go manifest rendering

ADR 0042: dbt support via native-Go manifest rendering

ADR 0043: TaskGroup as a first-class construct with split/fused execution

ADR 0043: TaskGroup as a first-class construct with split/fused execution

ADR 0044: dbt multi-project — one project per business domain

ADR 0044: dbt multi-project — one project per business domain

ADR 0045: Secrets reach a task because it declared them

ADR 0045: Secrets reach a task because it declared them

ADR 0046: Coverage — one rule, per package, counting the tests we already wrote

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 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 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 0049: Split the API/UI and scheduler into separate roles of one binary

ADR 0050: Model Context Protocol (MCP) server

ADR 0050: Model Context Protocol (MCP) server

ADR 0051: Separate the orchestration and execution state machines

ADR 0051: Separate the orchestration and execution state machines

ADR 0052: Durable task outcome — decouple the task result from report delivery

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 0053: Admission + placement — one scheduler-side layer for task concurrency and pod assignment

ADR 0054: Coexistence in a shared, multi-team Kubernetes cluster

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 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 0056: Task-log object sink — native dual-SDK (S3 + GCS), keyless-first

ADR 0057: OIDC/SSO authentication with fail-closed tenant pinning

ADR 0057: OIDC/SSO authentication with fail-closed tenant pinning

ADR 0058: Warm worker pools — pod-reuse semantics (N:1)

ADR 0058: Warm worker pools — pod-reuse semantics (N:1)

ADR 0059: OpenLineage emission from the Go control plane → OpenMetadata

ADR 0059: OpenLineage emission from the Go control plane → OpenMetadata

ADR 0060: External secrets resolution — a pod-side, provider-neutral SecretResolver (AWS reference)

ADR 0060: External secrets resolution — a pod-side, provider-neutral SecretResolver (AWS reference)

ADR 0061: Secret material never lands in the user's tree — private scratch, masked on read

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: Release branches per minor, main always open

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: Migrating an existing Lite install off the published encryption key

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: Patch content in 0.x is gated by safety, not by kind

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: Contributor context is public and tool-agnostic; tool files stay local

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: Secrets reach a process by environment reference, never rendered into a command string

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: Releases stage, verify, then promote; only a promote job moves public pointers

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: CodeQL blocks a pull request only for the high or critical alerts it introduces

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: DAG images are built without a container daemon first; a local daemon is the fallback

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: CI deploys with named, scoped, revocable API tokens and a built-in deployer role

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: The daemonless build of ADR 0073 ships in 0.5.4, and pushed images are read back through the registry

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: deploy reuses the image already in the registry under the content label

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: a pushed build splits into a dependency image and a code layer

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 image is built from wheels without a daemon

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: the CLI signs in with single sign-on through the server, which stays the only OIDC client

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: registry credentials from the cloud CLI session, after the docker config

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: the CLI signs in from a machine with no browser through a device sign-in served by Dexaflow

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: a warm pool follows the DAG's current version, started when it becomes current and drained when it stops being current

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 builder runs on Cloud Build first, through the gcloud CLI

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.