arcform 0.2.2

Local-first data pipeline engine for analytical workflows
# Arcform

> Local-first data analysis in one binary, for people and their agents.

Arcform's command, `arc`, is for anyone working with data, whether it is new or already in place, and whether the job is a quick look or the upkeep of a larger data asset. Batteries are included: one binary fetches data, models it in SQL on DuckDB, runs advanced analytics through its operators, exports the results, and keeps a local history of every change. The work is kept as a Protocol, a folder holding `arcform.yaml` and its SQL, which `arc run` runs again from the top. That makes arc a good framework for agentic data analysis: an agent that explores data through arc leaves behind a versioned Protocol anyone can re-run, not a scratchpad.

## Install

```sh
curl -fsSL https://install.meridian.online/arc | bash
```

This installs a release build for macOS (Apple silicon and Intel) or Linux (x86_64 and aarch64, glibc 2.35 or later) and links `arc` into `~/.local/bin`. When no `duckdb` is on `PATH`, or the one there cannot read SQL into tokens, it then installs DuckDB's v2.0.0 release build with DuckDB's own installer, into `~/.duckdb/cli/2.0.0`, and says what to put on `PATH` if another `duckdb` comes first. Set `ARC_SKIP_DUCKDB=1` (`curl -fsSL https://install.meridian.online/arc | ARC_SKIP_DUCKDB=1 bash`) to leave DuckDB alone. To build from source instead:

```sh
CXXFLAGS='-DDUCKDB_VERSION="v2.0.0"' cargo install arcform --locked --features mcp,bundled-duckdb
```

The `CXXFLAGS` setting is needed until duckdb-rs fixes [#917](https://github.com/duckdb/duckdb-rs/issues/917): without it the bundled DuckDB reports a development version.

What arc needs beside it:

- the `duckdb` command on `PATH`, which runs a Protocol's SQL steps (v2.0.0 recommended; arc accepts `>=2.0, <3`). It must be DuckDB's release build, or another build that includes DuckDB's autocomplete extension: arc reads each SQL step's tokens from that extension's `sql_tokenize` to check what the step installs, and refuses to run SQL steps on a `duckdb` built without it, such as Homebrew's 2.0.0 at the time of writing. The refusal names the fix: install the release build with `curl https://install.duckdb.org | DUCKDB_VERSION=2.0.0 sh`, then put `~/.duckdb/cli/2.0.0` first on `PATH` or set `ARC_DUCKDB_BIN=~/.duckdb/cli/2.0.0/duckdb`;
- [`finetype`](https://github.com/meridian-online/finetype) 0.6.54 or later, for the five FineType tools `arc mcp` serves and the operators that use it (a `datapackage_describe` step with `nominations:` needs 0.6.60 or later);
- [`uv`](https://docs.astral.sh/uv/), for the Python operators.

Both builds carry DuckDB v2.0.0 inside `arc` but leave out its ICU extension, so a time-zone function in `arc table describe` or `arc sql query` needs it installed once. With the recommended v2.0.0 `duckdb`, run `duckdb -c "INSTALL icu"` once; a `duckdb` of another version installs it where arc does not look.

## What's in the box

- **Ingress:** DuckDB's `read_*` functions, plus operators that fetch over HTTP, discover links on a page and unpack archives.
- **Modelling:** SQL steps run on DuckDB, and arc reads each query for the tables it loads and makes, so it knows how the steps connect and which ones a change makes stale.
- **Advanced analytics:** operators for text embeddings, UMAP projection and entity resolution with Splink, and any Python script of your own, run through `uv`.
- **Egress:** DuckDB's `COPY`, plus operators that export Parquet and publish to DuckLake.
- **Data description:** semantic types and validation through FineType, and Frictionless Data Package descriptors.
- **Local history:** every change arc makes to `arcform.yaml` is a version `arc history` can list and restore, with a log in the folder that travels with it through git, and a tool that writes files beside it, such as chart specs, can keep their history there too.
- **Agents:** `arc mcp` serves all of this to an AI agent over the Model Context Protocol.

## First steps

With a CSV of your own (here `orders.csv`, with `region` and `amount` columns):

```sh
arc create-protocol shop
cd shop
cp ~/Downloads/orders.csv .

# Record two steps. Recording writes SQL files and arcform.yaml; nothing runs yet.
arc sql record "SELECT * FROM read_csv('orders.csv')" --name orders
arc sql record "SELECT region, count(*) AS orders, sum(amount) AS revenue FROM orders GROUP BY region" --name revenue

# Run the Protocol: each step makes the table it is named after.
arc run

# Look at what the run made, read-only. Both print JSON.
arc table describe revenue
arc sql query "SELECT * FROM revenue ORDER BY revenue DESC"
```

`arc history list` shows each recording as a version, and `arcform-log.txt` in the folder lists them, so the record goes with the Protocol through git. The database lives outside the folder, in `~/.arcform/db`, unless `arc create-protocol --db <file>` names one.

## Using arc from an agent

Install arc as above, with `duckdb` (and `finetype` and `uv`, if you use them) on `PATH`. Then add it to Claude Code:

```sh
claude mcp add arc -- arc mcp
```

An agent started from a desktop app may not see your shell's `PATH`. In that case, give the absolute path of `arc` (`which arc` prints it) and a `PATH` that holds `duckdb`, `finetype` and `uv`, as in this `.mcp.json`:

```json
{
  "mcpServers": {
    "arc": {
      "command": "/Users/you/.local/bin/arc",
      "args": ["mcp"],
      "env": {
        "PATH": "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
      }
    }
  }
}
```

For Codex, add this to `~/.codex/config.toml`. A run can take longer than Codex's default 60-second tool timeout, so raise it:

```toml
[mcp_servers.arc]
command = "/Users/you/.local/bin/arc"
args = ["mcp"]
tool_timeout_sec = 600
```

Through these tools an agent can start a Protocol, look at its tables, query them read-only, record steps and run them, and every change it makes to a Protocol is a version in arc's local history.

| Tool | What it does |
| --- | --- |
| `protocol_create` | Make a new Protocol, as `arc create-protocol` does. |
| `protocol_run` | Run a Protocol and return its run record: each step's outcome, the assets and the engine. |
| `table_describe` | A table's row count, columns, types, null counts and five sample rows, read-only. |
| `sql_query` | Run SELECT statements against a Protocol's database, read-only, and return the rows. |
| `sql_recognise` | Say what arc reads a SQL statement as: an operation it holds, or none. |
| `sql_record` | Record a SQL statement as a new step. Runs nothing. |
| `operation_describe` | List the SQL operations arc holds, or describe what one takes. |
| `operation_record` | Record an operation, such as `filter-rows`, as a new step. Runs nothing. |
| `operator_describe` | List the operator catalogue, or give an operator's `with:` JSON Schema. |
| `infer` | Infer the semantic type of string values, through `finetype infer`. |
| `profile` | Profile each column of a CSV or JSON file, through `finetype profile`. |
| `taxonomy` | Browse FineType's type taxonomy, or export a type's JSON Schema. |
| `validate` | Validate CSV or Parquet data against a JSON Schema and report its quality. |
| `generate` | Generate synthetic sample data from FineType's taxonomy. |

---

## Design Principles

**Local-first.** Arcform runs on your machine: no managed services, no cloud accounts, no ops overhead. What it does need is a DuckDB CLI, the one `ARC_DUCKDB_BIN` names or else `duckdb` on `PATH` (see [Engine](#engine)), and whatever binaries your steps invoke.

**Asset-aware.** Inspired by Dagster's software-defined asset model, Arcform treats data outputs — not tasks — as the primary unit of work. The pipeline graph reflects data dependencies, not just execution order.

**Structurally transparent.** A SQL step is not a black box. Reading DuckDB's own parse of each statement, Arcform can inspect and decompose queries into their constituent parts — load operations, CTE dependencies, and export targets — enabling fine-grained lineage and partial re-execution.

**Composable by design.** Pipelines are defined in YAML and composed from discrete, reusable steps. Each stage has a clear input contract and output contract.

---

## Execution model

Arcform runs your steps on your machine, as you. There is no sandbox and no container — **running a Protocol you did not write is running a shell script you did not read.** This is deliberate; the table names the fields it covers.

| Field | How Arcform runs it |
| --- | --- |
| `command:` on a step | `sh -c <command>` |
| `command:` on a precondition | `sh -c <command>`, evaluated while Arcform works out what is stale — so it runs even when the step it guards is then skipped |
| `command:` on a hook (`on_init`, `on_success`, `on_failure`, `on_exit`) | `sh -c <command>`, the same path as a step |
| `sql:` on a step or hook | passed to the DuckDB CLI arc was told to run or found on `PATH` (see [Engine](#engine)), which reads and writes whatever the SQL directs |
| `op:` on a step | a catalog operator: in this process — which may itself spawn a tool (`datapackage_describe` runs `finetype`) — or `uv run --script` on a script embedded in the `arc` binary, which may spawn tools of its own too. No shell, and no confinement either |

Each of those inherits the environment Arcform assembled, including the `ARC_PARAM_*` values from `params:`, your dotenv files and `--param` — and the stdout of any earlier step that declared `output:`, which is captured into `ARC_PARAM_<OUTPUT>` for everything that runs after it. `arc registry run` fetches a Protocol and runs it through the same path, so read one before you run it.

### Engine

Arcform runs SQL through the DuckDB CLI, and finds it in this order:

1. `ARC_DUCKDB_BIN`, when set, names the executable. A program that ships Arcform sets it, because an app started from the desktop does not see the `PATH` a terminal does. A set value is final: if it names something that is not an executable file, the run is refused with the variable and the path in the message, and `PATH` is not tried instead.
2. Otherwise, `duckdb` on `PATH`.

The version preflight, the SQL steps and Arcform's reading of each SQL file run the same executable, each with `DUCKDB_AGENT_MODE=0`, so DuckDB's agent mode, which a variable such as `AI_AGENT` or `CLAUDECODE` turns on, does not change its output. A `command:` step that calls `duckdb` by name still gets the one on `PATH`; Arcform does not rewrite commands.

Arcform is tested on DuckDB `>=2.0, <3`, and a run with a SQL step on any other version is refused before a step runs. The oldest version it accepts is 2.0.0, the major release of the DuckDB library Arcform links: Arcform links DuckDB 2.0, and a database it creates cannot be opened by DuckDB 1.x, so a 1.x engine is refused even with `ARC_ALLOW_UNTESTED_ENGINE=1` set. Arcform 0.2.1 is the release for DuckDB 1.5 (`cargo install arcform --version 0.2.1`). A Protocol's `engine_version:` narrows that range and cannot widen it: `">=2.1"` refuses 2.0 as well, `">=1.2"` still refuses 3.0, and `">=1.2"` on DuckDB 1.5 is refused by Arcform's own range. Set `ARC_ALLOW_UNTESTED_ENGINE=1` to run on an engine above the range at your own risk; the run prints a warning naming the version and the range, and a Protocol's own `engine_version:` still applies. A development build such as `2.1.0-dev` is outside the range, except a development build of a released commit: Homebrew's DuckDB 2.0.0 prints `v2.0.0-dev88614 (Development Version) d54cbbe3df`, the commit of the v2.0.0 tag, and Arcform treats it as 2.0.0. Such a build cannot download community extensions, since the registry does not serve a development build's commit, so a run of a Protocol that installs one warns first, naming DuckDB's installer for the release build, which can. Homebrew's 2.0.0 is also built without DuckDB's autocomplete extension, so a run with a SQL step is refused on it before a step runs, as on any `duckdb` that cannot answer `sql_tokenize` (see [Install](#install)). When Arcform cannot read the engine's version it warns and runs.

Arcform reads what each SQL statement reads and produces from DuckDB's own parse of it, asked of that same executable; a statement DuckDB gives no parse tree for is read by Arcform's own parser, [sqlparser-rs](https://github.com/sqlparser-rs/sqlparser-rs). `ARC_SQL_READER` chooses: unset or `duckdb` is that default, `sqlparser` has Arcform's own parser read every statement (a way round a mistake in the DuckDB reading), and any other value is refused. `arc run` with a SQL step and no DuckDB 2 to ask is refused before a step runs; a command that reads the graph without running it, such as `arc sql record`, reads it with Arcform's own parser instead, and says so once, naming the missing autocomplete extension and the fix when that is why DuckDB gave no tokens. When a Protocol last ran on a DuckDB of another major or minor version, `arc run` says so before the first step, since results can differ between them.

A Protocol's SQL may install a DuckDB community extension only when the extension is on the [vetted list](docs/VETTED_EXTENSIONS.md). Before a step runs, Arcform reads the SQL file of every step and hook as the DuckDB CLI reads it, with the tokens that DuckDB gives for it, and refuses a Protocol whose SQL installs a community extension off that list, installs one from an address or another repository, or names a switch that hands the SQL to a parser an extension supplies (`allow_parser_override_extension`, `current_dialect`), which can read comments and strings by other rules. A file DuckDB gives no tokens for, or tokens Arcform cannot read, is refused. It reads each step's and hook's own SQL file again just before that step or hook runs, and before each attempt of a step with `retry:`, so a file an earlier step writes over, or writes where there was none, is checked too: a step refused there fails with exit 2 and names itself in `ARC_FAILED_STEP`, the steps before it keep their tables, and a pinned extension first found there is checked against its pin there. It reads the SQL as written and confines nothing: a `command:` step, or a dot command such as `.shell` or `.read` in a SQL file, can run a DuckDB of its own, a process an earlier step leaves running can write over a step's file after Arcform has read it, SQL held in a database the step reads, such as a view's body, is not read, and SQL that a step builds while it runs, or writes to a file other than a step's or hook's own SQL file, is not read, though Arcform warns and runs the step when its SQL calls `query()` or `json_execute_serialized_sql()` on anything but a string literal, imports a database, or names `.duckdbrc`.

A Protocol pins the build of each community extension it installs under the `extensions:` key of `arcform.yaml`, beside `engine_version:`: extension name, then the DuckDB version as DuckDB prints it (`v2.0.0`), then the platform as `PRAGMA platform` prints it (`linux_amd64`), then the SHA-256 of the installed file as 64 lower-case hex digits. Before any step or hook runs, `arc run` installs each pinned extension the Protocol's SQL installs `FROM community`, on the DuckDB the steps run on and without loading it, and refuses the run when the installed file's hash differs from its pin, naming `arc upgrade <name>`, or when the install fails or names no file. An extension with no pin for the engine's DuckDB version and platform draws a warning naming the `extensions:` key and `arc upgrade <name>`, and runs. `arc run` does not write a pin. `arc upgrade <name>` writes or replaces the pin of an extension on the vetted list for the machine's DuckDB version and platform, from the build the community registry serves: it runs `FORCE INSTALL` and `LOAD` of that extension on the DuckDB the steps run on and writes the installed file's SHA-256, keeping every other byte of `arcform.yaml`, with the file as it was left as an `arc history` checkpoint. An author runs it on each platform a Protocol runs on. A step can replace a pinned file during the run with `FORCE INSTALL`, `UPDATE EXTENSIONS` or a `command:`; arc hashes each pinned file again when the run ends and fails the run on a change, by which time the steps after that one may have loaded the file. An arc older than this change ignores the `extensions:` key and runs a pinned Protocol without checking it. A step that sets `extension_directory`, `extension_directories` or `home_directory` is refused. The [vetted list](docs/VETTED_EXTENSIONS.md#the-build-each-extension-is) gives the details.

---

## Pipeline Stages

Arcform pipelines are organised into four stage types, reflecting the natural flow of an analytical workflow.

### Pre-SQL
Preparation steps that operate outside the database.

- File retrieval via `curl`
- JSON and YAML transformation via `jq` / `yq`
- Data validation via JSON Schema (integrated with FineType)
- Remote storage sync via `rclone`

### SQL
Structured query steps executed against DuckDB.

- Data loading via DuckDB `read_*` functions
- Data modelling with standard SQL and CTEs
- Data export via DuckDB `COPY` functions

### Advanced Analytics
Compute-intensive steps for ML workflows.

- Vector embedding generation
- CatBoost model training and inference

### Export and Activation
Output steps that deliver results beyond the database.

- File exports via DuckDB `COPY`
- Remote sync via `rclone`
- Chart rendering
- Markdown report generation

---

## Architecture

Arcform models each pipeline as a directed acyclic graph (DAG) of data assets. Edges in the graph represent data dependencies between assets, not just task sequencing.

This distinction matters: when an upstream asset changes, Arcform knows which downstream assets are stale and can trigger selective re-materialisation rather than a full pipeline re-run.

For SQL steps, DuckDB's own parse of each statement, read from the DuckDB the step runs on, provides structural introspection — allowing Arcform to surface CTE-level dependencies and treat individual load and export operations as discrete graph nodes.

---

## Relationship to the Meridian Ecosystem

Arcform is part of the [Meridian](https://github.com/meridian-online) project family, alongside [FineType](https://github.com/meridian-online/finetype).

- **FineType** classifies and validates text data types, providing a transformation contract from raw text to typed DuckDB expressions.
- **Arcform** orchestrates the pipelines in which that data flows — from ingestion through modelling to output.

The two libraries are designed to complement each other. FineType's JSON Schema validation integrates directly into Arcform's Pre-SQL stage, enabling data quality checks as a first-class pipeline step.

---

## The log in a Protocol's folder

Each version arc's local history records of `arcform.yaml`, or of a file beside it such as a chart file under `panels/`, adds one line to `arcform-log.txt` in the Protocol's folder. The snapshots themselves stay outside the Protocol, in `$ARCFORM_HISTORY_DIR` or `~/.arcform/history`, where `arc history` lists, shows and restores them on the machine that recorded them. The log goes with the folder, through git, a synced drive or an archive, and says which file was recorded, when, through which interface and with which steps. A line holds no contents of any file. The ignore list `arc create-protocol` and `arc init` write does not name the log, so `git add` stages it beside `arcform.yaml`, and `arc history list` ends by saying where it is.

A folder renamed, moved or copied on the same machine keeps its versions. The snapshots are keyed to the folder's path, and `arc history` in the folder's new place finds them again through the log: each version the log names that this machine's store holds for a file of the same name is copied under the new path, so `arc history list` prints what it printed before and `arc history restore` writes the bytes it wrote before. A copy and its original list the same versions when the copy is made, and from the first version either records after it, each lists its own. A version is found by the id its line names and never by a file's contents, so a fresh Protocol with the same files, or a clone on another machine, lists from its own first version on.

Two people who share a Protocol through git record versions in their own clones, and both of their commits append to the log. `arc create-protocol`, `arc init` and `arc init --from-descriptor` write a `.gitattributes` beside the ignore list with one line naming the log, `/arcform-log.txt merge=union`, which tells git to merge the log by keeping both sides' lines. The merge then has no conflict on the log, and the merged log holds each clone's lines once. The lines stay in the order git leaves them, which is not time order, so a reader that wants time order sorts by `at`, the time each line carries. A `.gitattributes` already in the folder keeps its lines and gains that one after them, unless a line of it already sets the log's `merge` attribute, which is then left as written. A Protocol made before arc wrote the file, or by hand, has no such line, and the first merge of two clones' logs conflicts until the line is added to the folder's `.gitattributes`.

Each line is one JSON object, so the log is newline-delimited JSON that any JSON reader parses line by line:

```json
{"at":"2026-10-03T07:15:02.123Z","file":"arcform.yaml","kind":"save","interface":"app","version":"1791011702123-000-save","step":"big_orders","change":"changed"}
```

At that time `arcform.yaml` was saved in the app, as the version `arc history list` prints with that id, and the save changed the step `big_orders`. A save that touched several steps names each of them in one line:

```json
{"at":"2026-10-03T07:16:40.456Z","file":"arcform.yaml","kind":"save","interface":"app","version":"1791011800456-000-save","steps":[{"step":"tally","change":"changed"},{"step":"big_orders","change":"added"}]}
```

The keys:

- `at`: the time the version was recorded, RFC 3339 in UTC to the millisecond.
- `file`: the file's path inside the folder, with `/` between its parts.
- `kind`: `save`, `checkpoint` or `unsaved`.
- `interface`: the way arc was reached, `terminal` for the `arc` command line, `mcp` for a tool of `arc mcp`, or the word the app or another library caller names for itself. A version recorded with no way has no `interface`.
- `version`: the version's id, as `arc history list` prints it.
- `step` and `change`: the name of the one step the version touched and what happened to it, `added`, `removed` or `changed`. `arc operation record`, `arc sql record` and their `arc mcp` tools name the step they add. For a plain save of `arcform.yaml`, whether from the app or from a library caller, arc compares the saved text with the state its history held before and names each step the save added, removed or changed. A step that was renamed reads as one removed and one added.
- `steps`: instead of `step` and `change`, when a save touched more than one step, a list of objects each holding a `step` and its `change`.

A version with neither names the file and no step: a checkpoint, an unsaved text, a file that is not `arcform.yaml`, a save that touched no step, and a save of `arcform.yaml` when the history holds no earlier state to compare with, because it is new on this machine or because that state is gone. A key at the manifest's top, a comment, the order of a step's keys and the order of the steps are no change to a step.

The folder of a file not named `arcform.yaml` is the nearest directory at or above it that holds one, so a Protocol has one log, and each line names its file. A line is appended in one write, so a reader never sees half of one. A checkpoint taken before a write gets its line when the write lands, just before the save's, so a write arc refuses leaves the folder as it was. A version arc does not record, because its text equals the newest one, gets no line. A line outlives its snapshot: pruning and the merging of quick saves remove snapshots and never lines, so a line's id finds a version only on a machine that still holds it. When arc cannot write a line, because the folder is read-only, the version is recorded all the same.

---

## Using Arcform as a library

The `arcform` crate ships a library target alongside the `arc` binary, for one purpose: a tool that edits an `arcform.yaml` should not carry its own copy of the schema. Two schemas drift; one cannot.

```toml
[dependencies]
arcform = { version = "=0.2.2", default-features = false }
```

`default-features = false` turns off the `cli` feature. That feature exists for the `arc` binary; it also compiles a doc-hidden `cli_main` into the crate root, and `cli_main` parses the *calling* process's `argv` and can exit it. With the feature off the crate root is `spec` and nothing else, and `clap` leaves the dependency graph.

```rust
use arcform::spec::{Manifest, Result, SpecEdit, edit_spec};

fn example() -> Result<()> {
    // A spec on disk.
    let dir = std::path::Path::new("examples/brewtrend");
    let manifest = Manifest::load(dir)?;
    println!("{} — {} steps", manifest.name, manifest.steps.len());

    // Edit it through the write path: applied to the original bytes, validated
    // by the same loader, written atomically — or refused with a reason.
    let edit = SpecEdit::Replace {
        path: vec!["params".into(), "trend_threshold".into(), "default".into()],
        value: "\"25\"".into(),
    };
    edit_spec(dir, &[edit])?;
    Ok(())
}
```

`arcform::spec` is the entire public surface: the spec schema types, the loading entry points, the write path, and the error type. Validation is a **gate, not a transform** — it parses raw YAML to reach a verdict and hands nothing back to write. The write path is built on that: a `SpecEdit` is a plain value, applied to the **original bytes** by splicing at tree-sitter node spans, so every byte an edit does not target — comments, blank lines, key order, quote style — survives verbatim, and a result that will not load is refused with the loader's reason, leaving the file untouched. Writes are atomic (temp file + rename). `create_spec` serialises a brand-new manifest directly — a spec with no prior authorship has nothing to preserve — through the same gate. Serialising an *existing* spec yourself remains out of contract: load-serialise-write-back destroys the author's bytes, which is the whole reason the write path exists. Everything else — the runner, the engine, SQL introspection, the operator catalogue, the registry — is private and moves without notice.

The surface is documented as a contract in `src/spec.rs` and enumerated by `tests/public_surface.rs`, which fails the build if it widens. Arcform is pre-1.0: **pin an exact version** (or, inside the same organisation, a git revision).

Two build-time facts come with the dependency. Arcform parses SQL with its own `sqlparser` fork, published on crates.io as `arcform-sqlparser` and pinned to an exact version, so it resolves like any other dependency and the consuming workspace needs no patch entry. And the private engine still compiles into the library, so the build needs the DuckDB C library (`DUCKDB_LIB_DIR`/`DUCKDB_INCLUDE_DIR`) even for a consumer that only loads specs, or the `bundled-duckdb` feature, which compiles DuckDB in and takes tens of minutes from cold.

---

## Status

Pre-1.0. v0.2.0 is the first published release, and the command line, the `arc mcp` tools and the library's `spec` surface may still change between minor versions. [CHANGELOG.md](CHANGELOG.md) records each change.

---

## Credits

Part of the [Meridian](https://meridian.online) project.

Built with [DuckDB](https://duckdb.org), [sqlparser-rs](https://github.com/sqlparser-rs/sqlparser-rs) (SQL introspection), and [Serde](https://serde.rs).

Pipeline model and step execution inspired by [Dagu](https://github.com/dagu-org/dagu). Asset-centric design influenced by [Dagster](https://dagster.io/)'s software-defined asset model. SQL-first approach informed by [SQLMesh](https://sqlmesh.com/) and [dbt](https://www.getdbt.com/). Local-remote parity pattern drawn from [nektos/act](https://github.com/nektos/act).