Aqueduct
Self-healing data pipelines. Declarative. Observable. Autonomous.
One blueprint. Spark or DuckDB. Your data never leaves your servers.
Why Aqueduct
A pipeline fails at 3 a.m. over a column rename upstream. Somebody gets paged. They scroll a four-kilobyte stack trace to find a one-line fix. Aqueduct turns that night into a Git-diffable patch, waiting for review in the morning.
Wake up to a pending patch instead of a wall of errors.
- Declarative, not DAG code. Pipelines are YAML Blueprints. No PySpark boilerplate, no operator classes. Bring your own scheduler; Aqueduct is the control plane on top of the engine.
- Self-healing rather than alerting. On failure, an LLM agent diagnoses the root cause and emits a structured patch. The patch must clear guardrail, lineage, and sandbox gates before it touches your pipeline. The agent cannot generate code or touch a shell. A failure it has solved before heals from memory, with zero LLM tokens.
- Observable by construction. Every run, heal attempt, and column-lineage edge lands in a queryable store. Nothing is added to the hot path to make that happen.
- Model-agnostic and local-first. Anthropic natively, or any OpenAI-compatible endpoint: OpenRouter, DeepSeek, Groq, a local 7B on Ollama or LM Studio. Set
providerandbase_url, done. Point it at a model inside your perimeter and your data, schemas, and error traces never leave your servers. Multi-model cascades escalate to a bigger model only when the small one gets stuck. - The harness owns correctness; the model only proposes. The LLM does exactly one thing deterministic code cannot: turn an unstructured failure into a structured hypothesis. Everything after that is deterministic. A constrained patch grammar with 14 operations and no code generation. Validation gates. Budget caps. A signature cache. That division of labor is why a small local model holds up here when raw code-generating assistants don't.
Table of contents
- Supported engines
- What you get
- Core concepts
- The healing flow
- Architecture
- Observability dashboard
- Getting started
- How it compares
- References
- Contributing
Supported engines
|
Apache Spark Distributed batch at cluster scale. Delta Lake, JDBC, custom data sources, the full Blueprint grammar. |
DuckDB Single-node production, zero JVM. Ships with the base install. Same blueprint, same CLI, same healing loop. |
aqueduct run pipeline.yml --set deployment.engine=duckdb
One blueprint format, one CLI, per-engine execution. Each engine declares a capability table, and the compiler checks your blueprint against it. If the target engine can't do something, you get a compile error naming the exact capability instead of a runtime surprise.
Three promises, each enforced by machinery rather than review discipline:
- Every
supportedentry in the capability matrix is backed by a named test that runs on that engine in CI. - Nothing that can change your results is ever silently ignored. The engine honors it, warns about it, or refuses to compile.
- Healed blueprints record which engine produced each patch. Deploy a DuckDB-healed blueprint to Spark and the compiler tells you.
The two engines do not promise identical values, and Aqueduct does not pretend they do. SQL dialects genuinely differ; the known differences are cataloged in the Compatibility Matrix. Your Assert rules run on both engines, so the properties you care about are the properties that get checked. That is your portability contract.
Polyglot pipelines
Engines can also mix inside one blueprint. Set engine: on a module and everything downstream inherits it until another module overrides:
modules:
- id: extract # heavy join across two warehouses
type: Channel
engine: spark
config: { op: sql, query: "..." }
- id: aggregate # small result, no cluster needed
type: Channel
engine: duckdb
config: { op: sql, query: "SELECT region, sum(amount) FROM extract GROUP BY region" }
The compiler partitions the DAG into engine islands and inserts a handoff at each boundary. The handoff materializes data as parquet at a configurable location, visible in observability like any other module, with bytes and duration recorded. Each handoff point is announced at compile time, because the extra I/O is a real cost you should see before the run. If an island fails, a rerun picks up the already-materialized upstream data instead of recomputing it. Two independent flows with different engines run side by side with no handoff at all.
What you get
| Capability | What it does | Details |
|---|---|---|
| Self-healing | LLM diagnoses failures, emits gated, Git-diffable patches with human, CI, or auto approval | Spec §8 |
| Heal memory | Failure signatures cache validated fixes; repeat failures heal with zero LLM tokens | Spec §8.2 |
| Engine capability gate | Unsupported features fail at compile time with a named capability, never mid-run | Compatibility Matrix |
| Portable types | One type vocabulary across engines; ambiguous spellings rejected at parse time | Spec §9 |
| Heal provenance | Blueprints record which engine healed them; cross-engine deploys warn at compile | Spec §8.14 |
| Polyglot pipelines | Per-module engine choice; automatic, observable handoff at engine boundaries | Spec |
| Observability store | Runs, failures, heal attempts, metrics in queryable DuckDB/Postgres | Observability Guide |
| Column lineage | Compile-time, zero engine actions; powers the patch lineage gate | Spec §7 |
| Data quality | Inline Assert rules + Spillway quarantine: bad rows are routed to a typed error sink instead of being dropped |
Spec §4.4 |
| Module tests | aqueduct test runs transforms against inline fixtures, with no I/O and no cluster |
CLI Reference |
| LLM benchmark | aqueduct benchmark scores models against simulated failures so you can pick the cheapest model that heals your pipelines |
CLI Reference |
| Safety rails | Guardrails, multi-axis budgets, hourly heal caps, sandbox replay before any live write | Spec §8.3 |
| Observability dashboard | aqueduct dashboard, a local read-only Streamlit viewer: fleet, runs, lineage, healing patches with before/after diff, performance, quality |
Observability Guide |
Core concepts
| Concept | Purpose |
|---|---|
| Blueprint | Your pipeline definition |
| Ingress | Reads sources (CSV, Parquet, Delta, JDBC) |
| Channel | Transformations (SQL or native ops) |
| Egress | Writes sinks (overwrite, append, Delta merge) |
| Junction | Fan-out (conditional, broadcast, partition) |
| Funnel | Fan-in (unions, coalesce, zip) |
| Spillway | Routes bad rows to error sink |
| Probe | Non-blocking observability taps |
| Regulator | Gate driven by Probe signals (skip / abort / trigger agent) |
| Assert | Inline quality gates |
| Depot | Cross-run state & watermarks |
| Arcade | Reusable sub-pipelines |
Full details in the References.
The healing flow
When a pipeline fails, Aqueduct does not throw a stack trace at an LLM and hope. Healing is a staged, auditable pipeline. The model works inside a constrained grammar: it cannot write code, edit files, or run shell commands.
A generated patch clears four gates and a compile-check before it ever touches the Blueprint. Gate 1 is guardrails: deterministic policy checks on paths, operations, and confidence. The compile-check follows immediately, and the patched Blueprint must still parse. Then Gate 2, lineage: does the patch break a downstream column consumer. Gate 3, sandbox: replay against representative data. Gate 4, plan regression. They run in that order and the first failure wins:
✓ guardrails → ✓ compile-check → ✓ lineage → ✓ sandbox → ✓ plan-regression → patch applied
Every patch clears the pyramid before it touches the Blueprint. aqueduct patch preview --sandbox runs the same pyramid on demand, before you decide to apply.
Approval modes
Who applies a generated patch. Deterministic guardrails (allowed paths, forbidden operations, minimum confidence) bound every patch regardless of mode.
| Mode | Who applies the patch | When the Blueprint changes | Use when |
|---|---|---|---|
disabled |
LLM never fires | Never | Healing is intentionally off. |
human |
Engineer reviews and applies | Only after human accepts | Production. Default behind CI/CD. |
ci |
External CI receives patch, opens a PR | Only after merge | Production with code review. |
auto |
Aqueduct applies in-memory, re-validates, writes only if the re-run succeeds | Only on a successful re-run | Trusted environments: dev, scoped pipelines. |
Low-confidence patches and any guardrail violation auto-escalate to human review.
What a patch looks like (click to expand)
Every patch is a PatchSpec, a structured, Git-diffable JSON document staged under patches/pending/. It contains declarative operations against the Blueprint, never code or shell commands:
// patches/pending/hello-pipeline-20260611T031412.json (abridged)
{
"patch_id": "hello-pipeline-20260611T031412",
"run_id": "9f3c2e1a",
"category": "config_error",
"root_cause": "Ingress 'load' reads data/in.csv, but the upstream job renamed the file to data/input.csv.",
"confidence": 0.92,
"rationale": "PATH_NOT_FOUND on data/in.csv; a sibling data/input.csv exists with a matching schema, so the path is stale rather than the data missing.",
"operations": [
{ "op": "set_module_config_key", "module_id": "load", "key": "path", "value": "data/input.csv" }
]
}
Review it, then aqueduct patch apply, or let auto mode validate and apply it for you.
Why it holds up
- Every change is visible. A patch is a structured diff with a rationale and a confidence score. Low confidence escalates to a human.
- Live data stays safe. The sandbox validates each patch against representative data before any live write.
- Loops are bounded. A multi-axis budget caps wall-clock time, tokens, reprompts, and stuck-signature windows. A rolling rate limit caps heals per hour per blueprint.
- Decisions are auditable. Every LLM turn is recorded with the gate that rejected it, a stable error signature, and the prompt version. One run id joins every iteration of a heal.
- Efficient. Healing stops on the first successful patch. Structured error extraction replaces multi-kilobyte traces with a short root-cause block. Cheap lineage and sandbox checks reject bad patches in seconds, before any full-pipeline replay.
- Reach beyond the blueprint, with the same discipline. Healing extends past pipeline definitions into engine and session config (allowlisted keys only) and dependency declarations, each behind its own gate. Config patches are validated against a per-engine allowlist and replayed in the sandbox. Dependency fixes are declared and validated, then delivered for redeploy rather than live-installed. Data mutation stays off by default: the agent never touches your data without an explicit opt-in and per-action human approval.
For the stage-by-stage detail, see the Blueprint & Engine Spec.
Architecture
Where Aqueduct sits in a data platform: the control plane between your scheduler and the engine.
Inside the box, Aqueduct is a single CLI that runs on the driver, with no servers and no daemons. Logic flows through four immutable layers:
- Parser validates YAML into an immutable AST.
- Compiler resolves context, expands Arcades and macros, extracts column lineage, gates the blueprint against the target engine's capabilities, and assembles a fully-resolved Manifest.
- Executor runs the Manifest on the target engine. Engines register through an entry-point protocol; Spark code is isolated under
executor/spark/, DuckDB underexecutor/duckdb_/. The core never imports an engine by name. - Surveyor records runs, failures, and lineage to pluggable stores and triggers the Agent on failure.
Observability dashboard
aqueduct dashboard launches a local, read-only Streamlit viewer over the same observability store the engine writes to. It runs on demand, like the Spark UI. It is not a production server, and no pipeline requires it. One place for fleet health, per-run module metrics, column lineage, the self-heal patch stream with before/after diffs, performance trends, and data-quality signals across every blueprint. Backend-agnostic (DuckDB or Postgres). Every view re-reads with short-lived connections, so it can't block a running pipeline's writer.
pip install "aqueduct-core[dashboard]"
aqueduct dashboard # opens http://localhost:8501
Getting started
Installation
pip install aqueduct-core # DuckDB engine included, no JVM needed
pip install "aqueduct-core[spark]" # adds Apache Spark + Delta Lake
[!NOTE] Python 3.11+. Java 17 for the
sparkextra only (JAVA_HOMEmust point to it). Every release is CI-tested against three pinned combos: LTS (Python 3.11 · Spark 4.1), Latest (Python 3.13 · Spark 4.1), Legacy (Python 3.12 · Spark 3.5). Live results in the Compatibility Matrix.
Compose extras as needed, for example pip install "aqueduct-core[spark,airflow,aws]":
| Extra | Adds | Install when |
|---|---|---|
spark |
PySpark 4 + Delta Lake | Running pipelines on Spark on this host. |
duckdb |
numpy, for DuckDB Python UDFs | Writing a Python UDF that runs on DuckDB. The engine itself needs nothing. |
airflow |
Apache Airflow operator shim | Scheduler / worker host; the box submitting jobs. |
secrets |
AWS + GCP + Azure secret-manager SDKs (or pick aws / gcp / azure individually) |
Resolving @aq.secret('KEY') against a cloud vault. |
stores |
Postgres, Redis, and object-store backends (or pick postgres / redis / object-store individually) |
Replacing single-writer DuckDB defaults for obs / lineage / depot. |
llm |
json-repair, last-ditch recovery of malformed LLM patch JSON |
Healing with small local models that emit imperfect JSON. |
all |
Every runtime extra above, plus databricks |
Single-laptop dev. |
[!IMPORTANT]
allcovers runtime capabilities only. The two local dev tools ship separately on purpose, so a production install never pulls a web framework:pip install "aqueduct-core[dashboard]"for the Streamlit viewer andpip install "aqueduct-core[mcp]"for the MCP diagnostics server.
A first blueprint
aqueduct: "1.0"
id: hello.pipeline
name: Hello Pipeline
macros:
active: "status = 'active' AND deleted_at IS NULL"
modules:
- id: load
type: Ingress
label: Load orders
config: { format: csv, path: "data/in.csv", options: { header: true } }
- id: clean
type: Channel
label: Filter active
config:
op: sql
query: "SELECT order_id, amount FROM load WHERE {{ macros.active }}"
- id: save
type: Egress
label: Write parquet
config: { format: parquet, path: "data/out/", mode: overwrite }
edges:
- { from: load, to: clean }
- { from: clean, to: save }
agent:
approval: human
Run it on either engine:
aqueduct run blueprints/hello.yml # engine from aqueduct.yml
aqueduct run blueprints/hello.yml --set deployment.engine=duckdb # no cluster, no JVM
Engine-wide defaults live in a separate aqueduct.yml (target engine, LLM provider, store backends, danger settings). Inline module tests live in *.aqtest.yml. Repeatable healing benchmarks live in *.aqscenario.yml. The Gallery has runnable examples of each.
Five commands to know
aqueduct doctor blueprints/hello.ymlis the preflight check. It validates YAML, resolves paths, verifies LLM reachability, and opens stores.aqueduct run blueprints/hello.ymlexecutes the pipeline. On failure, the agent generates a patch underpatches/pending/.aqueduct patch apply patches/pending/<id>.json --blueprint blueprints/hello.ymlreviews and accepts a staged patch, moving it topatches/applied/.aqueduct test blueprints/hello.aqtest.ymlruns Channel / Junction / Funnel modules against inline data, without touching Ingress, Egress, or any external I/O.aqueduct benchmark gallery/aqscenarios/ --model claude-sonnet-4-6 --model qwen2.5-coder:7bcompares LLM models against simulated failures. No engine required.
Full reference in CLI Reference.
How it compares
| Aqueduct | dbt | Dagster / Airflow | Raw PySpark | |
|---|---|---|---|---|
| Pipeline definition | Declarative YAML Blueprints | SQL models | Python DAG code | Imperative code |
| Engine | Spark or DuckDB, capability-gated | Warehouse SQL | Orchestration only | Apache Spark |
| On failure | Autonomous LLM patch + gates | Manual fix | Retry / alert | Manual fix |
| Column lineage | Built-in, compile-time | Built-in | Plugin | DIY |
| Built-in observability store | Yes (DuckDB/Postgres) | Partial | External | DIY |
Aqueduct is a transformation engine with a repair loop attached. It is not a scheduler (pair it with Airflow via the airflow extra) and it is not a warehouse SQL tool.
References
- Blueprint & Engine Spec: module types, configs, architecture, type system, healing loop
- SKILL.md: distilled Blueprint-authoring guide for LLMs (grammar, patterns, provider base_urls)
- CLI Reference: all commands and flags
- Spark Engine Guide: warnings, performance, tuning
- Observability Guide: schemas + diagnostic query cookbook
- Production Guide: cluster deployment, security, Delta operations
- Compatibility Matrix: supported Python × Spark versions, per-engine capability tables
- Extending Aqueduct: how to add an execution engine (
ExecutorProtocol, capability declarations, entry points) - Gallery: real working examples
Contributing
Contributions are welcome! See CONTRIBUTING.md.
Aqueduct is Apache 2.0 licensed: free and open source, with no telemetry and no lock-in.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aqueduct_core-2.1.3.tar.gz.
File metadata
- Download URL: aqueduct_core-2.1.3.tar.gz
- Upload date:
- Size: 1.2 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eab05a991d4a4322562cd94ba1949df98df84c29a1a606c9e1253ba65e408d22
|
|
| MD5 |
fa32afd63724f017d0f8725e4df04009
|
|
| BLAKE2b-256 |
8ba2c1806587969b0723f36122bbd4b13d01018ba1cb444fcfbcc5dd108e0d9b
|
Provenance
The following attestation bundles were made for aqueduct_core-2.1.3.tar.gz:
Publisher:
release.yml on sadigaxund/Aqueduct
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aqueduct_core-2.1.3.tar.gz -
Subject digest:
eab05a991d4a4322562cd94ba1949df98df84c29a1a606c9e1253ba65e408d22 - Sigstore transparency entry: 2578359493
- Sigstore integration time:
-
Permalink:
sadigaxund/Aqueduct@b807df770a64e9302b30e8df126f5b041740c39f -
Branch / Tag:
refs/tags/2.1.3 - Owner: https://github.com/sadigaxund
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@b807df770a64e9302b30e8df126f5b041740c39f -
Trigger Event:
push
-
Statement type:
File details
Details for the file aqueduct_core-2.1.3-py3-none-any.whl.
File metadata
- Download URL: aqueduct_core-2.1.3-py3-none-any.whl
- Upload date:
- Size: 1.1 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
25ac9b764999ab107603ede4c3270c5f9851bf8c26edce1fa1e3bb662790902a
|
|
| MD5 |
d7569907a408b34dc5ff9d365bc97c6c
|
|
| BLAKE2b-256 |
16d0163264e96bf043ce4ad0ae3b1c30749b630558d08929f3ab7ff5259d571c
|
Provenance
The following attestation bundles were made for aqueduct_core-2.1.3-py3-none-any.whl:
Publisher:
release.yml on sadigaxund/Aqueduct
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aqueduct_core-2.1.3-py3-none-any.whl -
Subject digest:
25ac9b764999ab107603ede4c3270c5f9851bf8c26edce1fa1e3bb662790902a - Sigstore transparency entry: 2578359723
- Sigstore integration time:
-
Permalink:
sadigaxund/Aqueduct@b807df770a64e9302b30e8df126f5b041740c39f -
Branch / Tag:
refs/tags/2.1.3 - Owner: https://github.com/sadigaxund
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@b807df770a64e9302b30e8df126f5b041740c39f -
Trigger Event:
push
-
Statement type: