Release notes
The authoritative, complete history is CHANGELOG.md in the repository. This page carries the current release in prose, with links into the docs that describe the new behaviour.
Check your installed version with:
valved version0.5.2 — 2026-09-01
Section titled “0.5.2 — 2026-09-01”Documentation only — no code change. The docs site publishes on a tag and nothing else, so a docs fix reaches you only by cutting a release. Upgrading gains you nothing functionally; skip it if you are not reading the docs.
What changed
Section titled “What changed”-
Deploying on Snowpark Container Services is now on this site. 0.5.0 shipped the deployment surface and this site never mentioned it — the only route here was a release-note link into the repository, which is private, so it went nowhere. The guide covers the five properties of Valved that decide the deployment’s shape, the three state-store options and what you actually trade between them, and the six-step shape of a deploy. The full operational runbook stays in
deploy/spcs/README.md. -
The CLI reference caught up.
valved cancelwas missing entirely, and--max-turnsandlogs --followhad never been documented.--max-turnsnow says what it is — a runaway-loop backstop, not a spend cap, so raising it does not make a stuck agent finish — and the page records that--database/--schemaare refused on a routed plan.
0.5.1 — 2026-09-01
Section titled “0.5.1 — 2026-09-01”Two concurrency fixes. Nothing to change in your project; upgrade and carry on.
What changed
Section titled “What changed”-
Pipelines with several dlt steps no longer fail intermittently. A dlt pipeline’s working directory is not safe for two processes to share — each one’s normalize stage picks up whatever extract packages it finds there, including a sibling’s, and whichever gets there first moves them. The others then die with
NormalizeJobFailed.Since the DAG runs independent steps concurrently, any pipeline with two dlt steps could hit this, and two steps on the same component hit it by construction. Each step now works in its own dlt data directory. Incremental state is unaffected — the directory is per step, not per run, so dlt’s local state survives across runs as before.
dlt step failures also now report a parsed reason and summary rather than leaving you to read the load package yourself.
-
valved serveshuts down cleanly under load. A worker cancelled at the moment it was registering could be left markedactivein the state store with nothing running behind it, until the reaper eventually reclaimed it. Visible as a pool that reads larger than it is after a hard stop — a second Ctrl-C, or the drain grace period expiring.
Upgrading
Section titled “Upgrading”pip install -U valved, then valved version should print 0.5.1. No configuration or migration changes.
0.5.0 — 2026-08-31
Section titled “0.5.0 — 2026-08-31”Valved can now run inside your Snowflake account. Plus the dlt engineer can finally run the dlt it writes, and --help stopped deleting the config-block names it was trying to tell you about.
Minor rather than patch: 0.4.1 through 0.4.4 were fixes only, and a new deployment mode does not belong under the same heading.
What changed
Section titled “What changed”-
Valved on Snowpark Container Services.
valved serve— the REST API plus the scheduler, reaper, archiver and worker pool — now runs as a long-lived SPCS service, so scheduled pipelines fire inside your Snowflake account with no machine of yours left running.See Deploying on Snowpark Container Services for the shape of the deployment and the five properties of Valved that decide it. Everything to stand one up ships under
deploy/spcs/: a pinned, non-root image with the dbt engine pre-baked,setup.sqlfor the Snowflake-side objects, and service specs both with and without a bundled Postgres. Ingress is OAuth-backed, and/api/docsserves Swagger UI same-origin because the ingress CSP blocks the CDN it would otherwise load from. -
The dlt engineer can run what it authors. It had always been told to author and verify — execute the component, read the load package, fix, repeat until green — and nothing in its toolset could execute anything. The result was not a graceful degradation: told to loop until green with no way to reach green, it spent its whole turn ceiling and wrote no files at all. Three
valved buildruns in a row against a real account ended that way.Builds now verify by actually loading. The verdict comes from the on-disk load package rather than an exit code, so a component that runs cleanly but loads nothing is a failure rather than a false pass — and a leftover package from an earlier run is never mistaken for this one’s.
If you build dbt models, run
valved connectfirst. Without a provisioned engine the dbt engineer is correctly withheld its execution tool and declines to claim a green it never observed — but only after a full build has been paid for. The smoke checklist now says so as its own step. -
A failed build tells you what it was doing. A routed build previously printed nothing between its first line and its last, and delegated agents wrote no log rows, so a build that exhausted its turn ceiling left a one-line error and no trace. Progress lines and per-tool logs now flow for engineers, reviewers, and routed plans alike.
-
--helpstopped eating config block names.--forceoffered to “Overwrite an existing section” and--same-repoto “drop the block” — a bare[snowflake.<name>]was being read as a style tag and deleted. Eight strings were affected. The sentences still read, which is why nobody noticed; the answer was just missing.
Upgrading
Section titled “Upgrading”pip install -U valved, then valved version should print 0.5.0.
One behaviour change to know about: valved build --database/--schema are now refused on a routed (multi-engine) plan instead of silently ignored. They could not confine where a dlt load landed — the schema comes from the component’s dataset name, which the plan chooses — and a flag reached for as a guardrail that does not guard is worse than no flag. Set the destination in the plan, or point the target’s <TARGET>_SNOWFLAKE_DATABASE/SCHEMA. Single-engine plans are unaffected.
0.4.4 — 2026-08-26
Section titled “0.4.4 — 2026-08-26”valved init will no longer quietly migrate another project’s database. Plus a Secrets column that has been printing raw markup since 0.4.0, and a cost ledger that recorded the most expensive failures as free.
All three were found by running the release smoke checklist against 0.4.3, and ship the same day it did.
What changed
Section titled “What changed”-
valved initno longer migrates a state store it did not create. This is the one to read if you run more than one Valved project on a machine.Every project scaffolded with the bundled Postgres resolves to the same
127.0.0.1:5432/valved. So runningvalved initin a fresh directory while another project’s Postgres was up connected to that project’s state store and ran migrations against it — unprompted, and reported as nothing more thanstate store schema initialized (postgres). Alembic upgrades are not generally reversible in place, so a mismatched pair of versions could have left the other project’s database upgraded with no way back and no indication it happened.initnow looks before it migrates. A database that already holds a Valved schema is adopted only when you say so:- pass
--adopt-existingto take it over deliberately, or - re-run
initwhere avalved.tomlalready exists — that is the project’s own claim on the store, so upgrades after apip install -U valvedstill work without a flag.
Otherwise
initstops with exit code 2 having changed nothing, tells you what it found, and points atVALVED_POSTGRES_PORTas the fix. If you run several projects locally, give each its own port — setVALVED_POSTGRES_PORTand the matching port insideDATABASE_URL; they have to agree. See running more than one project.Worth knowing regardless of migrations: sharing one state store between projects was never safe. Pipelines are keyed by name alone, so two projects’ pipelines with the same name are the same row.
Upgrade note: if you have CI that re-runs
valved initagainst a database that survives between runs, it will now exit 2. Add--adopt-existing. - pass
-
valved target listshows a rendered Secrets column again. It had been printing a literal[green]✓ all set[/green]instead of a green check. The helper returned styling markup but was annotated as returning a plain string, so the caller did the correct thing with a plain string and escaped it. Neither line was wrong on its own; the annotation was. Introduced in 0.4.0 and present in every release since. -
A failed agent run now reports what it actually spent. When a delegated agent raised — most importantly on hitting its turn ceiling, which means it looped for its entire budget — there was no result to read tokens from, so the run was recorded at
$0.00.valved metrics costsandvalved metrics agentsshowed the most expensive thing the harness does as free.Failed runs now carry their real tokens and cost. A failure that raised before reaching the API still records a genuine zero, because that is what it spent.
Upgrade note: failed runs now count against the recovery flow's daily cost cap, so recovery stops sooner after a burst of failures. That is deliberate — the cap was previously blind to exactly the spend it most needed to see.
0.4.3 — 2026-08-26
Section titled “0.4.3 — 2026-08-26”If you pause a schedule, it now stops immediately — including mid-tick. And valved build no longer gives up partway through a real build.
Three concurrency fixes in the scheduler and its state store — all the same family as 0.4.2’s: writers that read a row and then acted on what they had seen, without checking it was still true — plus a build that could not finish.
What changed
Section titled “What changed”-
valved buildno longer fails on a hardcoded turn limit. Every agent the build delegates to shared a ceiling of 30 turns, fixed in code with no setting and no flag. That was too low for real work: a freshly scaffolded project building one dlt source plus one staging model failed there every time, reporting onlyAgent exceeded max turns (30). The agent was not stuck — it was writing files and ran out of budget before it could hand them back.The ceiling is now
[runner.agent] max_turnsinvalved/runner.toml, withvalved build --max-turnsas a per-run override, and the default is raised. It is a runaway-loop backstop rather than a spend cap: an agent stops when it finishes, so a healthy build costs the same at the old ceiling or the new one.The failure message tells you something now, too — which agent ran out, which files it had already written (they are left on disk and are incomplete), and how to raise the limit. Previously working that out meant reading the database.
-
A paused schedule no longer fires one last time.
valved schedule pausetakes effect at once, but the scheduler reads its due-list a moment before it fires. A pause landing in that gap did not stop the fire — the fire’s guard was on the schedule’s next firing time, andpausedeliberately leaves that alone soresumepicks up without recomputing. So a pipeline could run once more after you had stopped it, and you pause when something is wrong, which is when an extra run costs most.When a pause wins, the firing time is left un-advanced, so
resumepicks that window back up rather than skipping it. The skip is recorded as aschedule.skippedevent with reasonpaused. -
valved serve --interval 0is rejected instead of spinning. The three loop-interval flags were unbounded, bypassing the validation on the equivalent config field. A non-positive value doesn’t crash — the boundary-sleep helper returns zero and the loop runs as fast as Postgres will answer. All three now floor at 1 second and refuse anything lower at parse time, naming the bound. Refused rather than quietly rounded up: a0is a typo worth reporting, not correcting. -
valved schedule set-cronon a brand-new pipeline no longer races into a traceback. Creating a schedule row took a lock that, by definition, locks nothing when the row doesn’t exist yet. Two callers creating the same pipeline at once both tried to insert, and the loser surfaced a raw database error. It now retries once — provably enough, since the row can only be created once, so the retry finds it and updates instead.
A correction to the 0.4.2 notes
Section titled “A correction to the 0.4.2 notes”Those notes said the archiver “has not been audited” for the multi-process exposure the scheduler had. It has been now, and that wording was more pessimistic than the code deserves.
The archiver moves each batch inside a single transaction that counts the rows, copies them, verifies the copied count matches, and only then deletes. A mismatch raises before the delete and the batch rolls back. A second archiver running against the same batch fails loudly and changes nothing, rather than losing or duplicating rows.
That’s from reading the code, not from a concurrency test — a distinction worth stating rather than blurring.
Running two full valved serve supervisors is still not a supported deployment. That hasn’t changed: leader election isn’t shipped, and the worker pool and API haven’t been examined for the same class of assumption. One valved serve plus standalone valved worker processes remains the way to scale out.
0.4.2 — 2026-08-24
Section titled “0.4.2 — 2026-08-24”If you run more than one valved serve against the same state store, a cron schedule could fire the same window twice.
A single-process deployment was never exposed. This matters once the state store lives somewhere two machines can reach — a container and a developer laptop pointed at the same Postgres is enough.
The scheduler stamped a schedule’s next firing time unconditionally: it re-read the row without a lock and wrote regardless of what it had originally seen. Two schedulers polling the same boundary both saw the same due schedule and both fired it. Scheduler wakeups are aligned to wall-clock boundaries, so two processes tick in near-lockstep rather than at random offsets — the collision is likelier, not rarer.
The job queue did not catch it. Its “one queued job per pipeline” guard covers queued jobs only, so it stops applying the moment a worker picks the first job up; from there the second scheduler’s enqueue is perfectly legal. The two runs then serialize, which is why this shows up as a pipeline that ran twice back to back rather than as an error.
What changed
Section titled “What changed”- A scheduled window fires exactly once, across processes. Advancing the next firing time is now a claim — a row lock plus a conditional swap on the value the scheduler observed when it read the due list. The scheduler that loses skips its fire and records a
schedule.skippedevent with reasonwindow_already_claimed. The claim happens before the run is enqueued, because a scheduler that has already enqueued a duplicate cannot take it back. - A double fire no longer skipped the following window too. The old stamp re-anchored on whatever it re-read, so the second scheduler advanced past the next tick as well — one window ran twice and the one after it did not run at all. Worth knowing if you are reconciling a gap in run history.
- A fire that races a schedule change now loses to it.
set-cronandreseedboth recompute the next firing time, so a fire in flight when you change a schedule is discarded rather than applied against the old cron.
One limit worth stating
Section titled “One limit worth stating”Running two full valved serve supervisors is still not a supported deployment. This release fixes the scheduler’s double-fire specifically. The job queue’s claim and reclaim paths were already safe, but leader election is not shipped and the archiver has not been audited for the same exposure. For redundancy today, run one valved serve and scale out with standalone valved worker processes — those claim from the queue safely.
0.4.1 — 2026-08-22
Section titled “0.4.1 — 2026-08-22”If you run dbt through valved run or a schedule, this is the release that makes it work.
A dbt step in a pipeline run looked for a bare dbt on PATH rather than the engine valved connect had provisioned. On a project with a pinned engine sitting right there, the step died with [Errno 2] No such file or directory: 'dbt'.
Nothing changes if you only ran dbt through valved plan / valved build verification — that path was always correct, and that is the whole story. Valved has two ways to execute the same work, and only one had ever been exercised against a real warehouse. The other was tested against the creds-free DuckDB substrate, which needs no credentials, no target identity and no managed engine, so its tests stayed green the entire time it could not work.
Found by running a real pipeline end to end for the first time: a 39-minute SEC EDGAR ingest that succeeded, followed immediately by a dbt step that could not start.
What changed
Section titled “What changed”- A pipeline’s dbt step uses the component’s pinned engine. Resolution is now one implementation shared by the build path, the runtime path and
valved connect— including the dialect, which is part of the engine directory name, so the writer and both readers cannot disagree about where the binary lives. - Missing engines fail loudly. With no pin, the
PATHlookup remains — right for a local DuckDB project. With a pin but no installed engine, the step fails namingvalved connect, instead of falling through to whateverdbthappens to be around. That matters most in a container: an image carrying an unrelated dbt, wrong version or wrong adapter, would previously have run it with no error at all. [components.*]reaches the runtime. The worker built its context without the project’s component blocks, so every dbt step ran as though none were configured. Any component whose name differed from the convention-detected dbt project failed with “dbt component did not resolve” — which reads as a configuration error and is not one. This is the gap most likely to surface when you split components into their own repositories.- A managed dbt backend no longer runs local dbt and reports success. A component naming
dbt-cloud,snowflake-nativeorremotesilently executed local dbt and returnedsucceeded. The build path has always refused this cleanly; the runtime now does too.
Two limits worth knowing before you containerize
Section titled “Two limits worth knowing before you containerize”Neither is new — both are stated because putting Valved in a container is when they start to matter.
- A dlt component’s dependencies are not installed at run time. The step executor runs the component’s entrypoint but never builds a virtualenv from its
requirements.txt, so its imports must already be present in the worker’s environment. In a container, the dependencies have to be in the image. - A
sqlstep’s connection is its literalconnectionvalue, not the run’s target. Running the same pipeline against a different target does not redirect itssqlsteps.
0.4.0 — 2026-08-21
Section titled “0.4.0 — 2026-08-21”Two things Valved documented all along and could not actually do, it now does: a pipeline can declare its own schedule and get one, and Valved can compose a pipeline at all.
Read strictly these are defect fixes — neither was held back for a later release, both were specified and written up and simply not wired to anything. It is a minor because of where you stand rather than where the code stands. These are not repairs to something that worked; they are two capabilities that start existing.
Read this before you rely on a seeded schedule
Section titled “Read this before you rely on a seeded schedule”A schedule only fires while valved serve is running. Seeding writes the row in the schedules table. The loop that reads that row and enqueues the run is the scheduler inside the supervisor — so if nothing is serving, nothing fires.
This is the difference between the feature working and it looking like it works. A pipeline can hold a perfectly correct schedule whose next fire time passed hours ago and have run zero times, and nothing in valved schedule list will say so — it shows the cron, the timezone, the target and the paused flag, but not whether the schedule has ever fired. The only tell is a next fire time that has quietly gone stale.
valved serve # scheduler + worker pool + API, all in one processvalved schedule list # cron, timezone, target, next fire — but not "is anything running?"Nothing is lost while you are not serving. A due schedule is any unpaused row whose next fire time has passed, so an overdue one fires on the next valved serve start rather than waiting for the following cron boundary. See Serving the control plane.
A pipeline’s [seed_schedule] now creates a live schedule
Section titled “A pipeline’s [seed_schedule] now creates a live schedule”Declare it in pipelines/<name>.toml and start the supervisor:
[seed_schedule]cron = "0 3 * * *"target = "prod"timezone = "America/New_York"On boot, valved serve walks pipelines/*.toml and writes a schedules row for every pipeline that declares the block and does not already have one. Previously the block was validated and displayed and then applied to nothing — the only way to get a live schedule was to type valved schedule set-cron by hand.
Editing the block later does nothing, on purpose. Seeding is first registration, once. It never touches an existing schedule row — not the cron, not the target, not a pause — because the live schedule is data you own through valved schedule, and a pass that re-applied your code over it on every boot would silently undo every set-cron anyone had made. On the already-present path nothing at all is written, not even an audit entry.
To push an edited block over the live values, there is now an explicit command:
valved schedule reseed <pipeline> # or POST /api/v1/schedules/{pipeline}/reseedThat one deliberately overwrites the live cron, timezone and target and records a reseed entry in the audit trail. Your pause state is not seedable and survives it. It was previously a stub that exited non-zero. MCP clients build their tools from the live OpenAPI document, so it reaches them as a schedule_reseed tool too.
Two limits worth knowing:
- Boot only. A pipeline file that appears while
serveis running gets seeded on the next restart. Usevalved schedule set-cronto stand it up immediately in the meantime. - A schedule whose pipeline file is gone is reported and left alone — not paused, not deleted. A file missing from one checkout is not evidence that the schedule should stop.
One unreadable pipeline file cannot starve the others or stop the supervisor coming up: each pipeline is reconciled on its own, and failures are named at boot and stepped over.
Valved can compose a pipeline
Section titled “Valved can compose a pipeline”The pipeline engineer — the agent that turns “run these components, in this order, daily” into a pipelines/<name>.toml — could not create that file. Its only write tool was one that edits existing files and refuses to create them. A pipeline is composed only after its components exist, so every composition is a new file, so every composition failed. It can create files now, through the same write-path check its edit tool already went through — the grant changes what it may do, not where it may write.
This was visible for a while before it was understood. The 0.3.1 docs audit found pipelines/<name>.toml described across five pages as something plan/build would write for you, established from the tool grants that it could not, and corrected the docs. Nobody connected “the docs are wrong” to “then the capability does not work.”
Also in this release
Section titled “Also in this release”- A build that no reviewer covered no longer tells you the review was clean. Reviewers are picked per engine — a dlt slice draws
dlt-qaanddlt-security, a dbt slice drawsdbt-qa— and a pipeline composition draws none, so it genuinely ships ungated. The gate logged exactly that, and the build summary printedBuilt; review clean.on the next line anyway, because it keyed off an empty findings list, which is also what a real clean review returns. An ungated build now saysBuilt; NOT reviewed — no reviewer covers this build's engines, and a clean one names who looked:Built; review clean (dbt-qa, dlt-qa).A pipeline composition is still unreviewed — it is checked by the harness’s validate loop, not by a reviewer — the reporting just stopped claiming otherwise. - CLI output no longer silently drops bracketed text, or replaces an error with a traceback. The console reads
[...]as markup, so an untrusted value interpolated into a message lost a[word]span outright, and a[/]raised — which, inside an error handler, meant you got a traceback instead of the error you were meant to read. Two instances had been fixed reactively, both found by accident; a sweep of the whole CLI found 420 more across 37 files and fixed them. A check now rejects any unescaped path to the console, so the class stays closed. - The docs site build fails on a dead internal link. Building the site did not validate internal links: a link to a page that does not exist, and an anchor to a heading that does not exist, both built successfully. The check runs inside the build itself rather than as a separate CI step, so a local build, the pull-request job, the pre-publish build behind a tag, and the hosting build all gate on it — and a tag carrying a dead link can no longer become these published docs. Anchors are checked too. No existing dead links were found; this is prevention, not a repair.
0.3.1 — 2026-08-20
Section titled “0.3.1 — 2026-08-20”A patch release about silence in both directions: work that was silently not happening, and work that was silently happening. Nothing here requires an edit to your project, but one behaviour changes on upgrade without announcing itself.
Valved no longer sends dbt’s anonymous usage statistics to dbt Labs
Section titled “Valved no longer sends dbt’s anonymous usage statistics to dbt Labs”dbt-core reports to dbt Labs on every invocation by default, and Valved runs dbt on your behalf — a build’s verify-by-execution, a dbt pipeline step. So a project whose owner never chose to run dbt directly, and was never told Valved would, had that project’s activity reported to a third party from the owner’s own machine or CI runner. There was no disclosure and no way to turn it off.
Valved now sets DO_NOT_TRACK=1 and DBT_SEND_ANONYMOUS_USAGE_STATS=False on every dbt child it spawns — both routes: the dbt-execution backend, which the build’s verify and the pipeline step go through, and the agent bash tier, which allows dbt run/build/seed/test/snapshot. Both variables, because dbt honours each and which one wins has varied across dbt versions. Measured from outside the process, where the connection is observable: one outbound connection by default, zero with the variables set.
On the bash route this also restores a choice that was being taken away: the environment an agent’s shell command inherits is default-deny, so a user who had set DO_NOT_TRACK=1 in their own shell had that opt-out stripped before Valved ran dbt. It is a forced value now, not an allowlisted one.
This is what you get on upgrade, with no action — and it is not currently overridable for dbt that Valved spawns. The variables are layered over the inherited environment, so an ambient DO_NOT_TRACK=0 does not win; and dbt applies DO_NOT_TRACK after it reads project flags, so send_anonymous_usage_stats: true in your own dbt_project.yml does not turn it back on either. If you actively want Valved’s dbt runs reporting to dbt Labs, there is no supported way to re-enable that today.
dbt you run yourself, outside Valved, is untouched. This governs only the dbt Valved runs for you.
valved plan --refine keeps its plan’s routing
Section titled “valved plan --refine keeps its plan’s routing”valved plan classifies a goal, routes it to domain engineers, and records planned_by_engine; valved build reads exactly that field to decide what to author. A refine never routed — it fell through to the monolithic extract-load planner, whose schema cannot express planned_by_engine at all. Refining a dbt goal handed you a dlt pipeline, and build accepted it.
It was invisible from the one surface built to show it: the refinement diff did not list the routing fields, so it reported that nothing had changed while the artifact kind changed underneath. And it bit hardest exactly where Valved points you at it — when an engineer ends a plan with a question, build refuses and prints valved plan --refine <plan_id> "<answer>" as the remedy, a verb that could not deliver the answer.
A refine now inherits its parent’s path, read off the parent and never re-derived from your feedback text. A routed parent refines through the routed path: the same engineers on the same slices, each handed its own prior design, its own question if it asked one, and your feedback. A parent with no recorded routing refines through the single-engine planner, unchanged — the rule cuts both ways, so an extract-load plan cannot become routed via --refine either. Your answer now also reaches the engineer that actually writes the code, as the same string the review gate is handed.
A refine still cannot add, remove or change an engine slice. It inherits the decomposition whole, so feedback asking for a new kind of work — “also pull the raw data with dlt”, against a dbt-only plan — has nowhere to land and is not acted on. That is the deliberate cost of not re-decomposing, but it is no longer silent: every routed refine stamps inherited_routing on the design and prints the limitation above the refinement diff. Run a fresh valved plan "<goal>" for work the plan was never routed for. See valved plan.
Also in this release
Section titled “Also in this release”- A class of dbt regression is now caught before it ships. The tests that check Valved’s model of dbt against actual dbt were gated on dbt being importable, and CI never installed it — so from the day they were written they reported SKIPPED on every run. They run on every pull request now. One of them covers your first five minutes: that
valved init --with-dbtscaffolds a project whose very firstdbt buildis green. - Docs corrections. An audit against the real interfaces found claims that were not merely stale but wrong — most visibly a landing page advertising every action as available “four ways with full parity” across CLI, REST, MCP and the web UI, when project setup is CLI-only and
plan --refinehas no REST equivalent, and agents advertised as armed with aweb_searchtool that no shipped agent is granted.
0.3.0 — 2026-08-19
Section titled “0.3.0 — 2026-08-19”The release that makes dbt work against a real warehouse end to end: valved connect → plan → build → models materialized in Snowflake.
Almost none of the defects it fixes were visible to the test suite, because almost none of them failed. An agent handed an empty in-memory DuckDB instead of your warehouse runs every query successfully, gets zero rows, and reports the emptiness as a fact about your project. A loud error replaced by a silent, believable emptiness — that shape is most of this release.
Read this before upgrading
Section titled “Read this before upgrading”Five behaviours change on upgrade without announcing themselves. None requires an edit to your project, but each behaves differently than it did in 0.2.1.
valved ask now issues real queries against your warehouse
Section titled “valved ask now issues real queries against your warehouse”Previously the explorer’s sql grant bound to an empty in-memory DuckDB, so every ask was answered from nothing — even an explicit --target prod, which only labelled the ask row. The grant now reaches the resolved target’s real connection.
This affects the CLI, POST /api/v1/asks, and therefore every MCP client’s ask tool. An ask that scopes no target resolves the project default, so unscoped asks hit the warehouse too. In practice: asks are slower, they consume warehouse credits, and they need working credentials. The ask still runs in read_only mode, which structurally forbids every write.
An unreachable warehouse now raises inside the explorer’s own sql call, where the agent sees the error and relays it. Earlier docs claimed a fail-soft fallback to the empty substrate; that handler was unreachable and the behaviour never existed. Both the claim and the dead code are gone — falling back would turn a visible failure into a successful list_schemas returning ['main'], which no agent can tell apart from a genuinely empty warehouse.
See ask in the core loop.
dbt_verify can materialize into your warehouse during valved build
Section titled “dbt_verify can materialize into your warehouse during valved build”Where a [components.dbt] block pins dbt_engine and dbt_version and valved connect has installed that engine, the dbt engineer is granted dbt_verify and can run build, run, seed or snapshot — all of which issue CREATE OR REPLACE TABLE against the target — while verifying what it authored. This is intended (an engineer that cannot execute cannot honestly claim green), but it means valved build may now write to the warehouse where previously it only wrote files.
It is bounded: select is mandatory so only the authored slice is built, valved plan clamps its subagents below build mode and therefore materializes nothing, and a project with no pinned-and-installed dbt engine is not granted the tool at all. See Build in the core loop.
Recovery’s daily cost cap now counts reviewer tokens, so it trips sooner
Section titled “Recovery’s daily cost cap now counts reviewer tokens, so it trips sooner”The cap (daily_token_budget_usd, $5.00 per day) reads the day’s total agent-attributed spend, and reviewer invocations were previously absent from that table entirely. Review spend that was always real but never recorded now counts against it. Once the day’s spend reaches the ceiling, recovery logs the failure and skips diagnosis until UTC midnight — so a build-heavy day can silence scheduled recovery for the rest of that day.
Note that this value is not configurable from a file today: no config file populates the [recovery] section, so a [recovery] block in valved.toml is accepted and ignored. See Recovery.
POST /api/v1/asks validates target and can return 400
Section titled “POST /api/v1/asks validates target and can return 400”target now reaches target resolution rather than only labelling the ask row, so a free-form label that used to be stored returns 400 with type slug target-resolution. Targets matching the normal name rules (^[a-z][a-z0-9_]*$) are unaffected; this only rejects values that were never usable as a target. See the REST API’s asks endpoints.
valved connect re-provisions existing dbt engine installs
Section titled “valved connect re-provisions existing dbt engine installs”The install directory key changed from <engine>-<version> to <engine>-<version>-<dialect>-<adapter version>, so an engine installed by 0.2.1 under .valved/engines/ no longer matches the name the installer looks for. The next valved connect builds a fresh venv — a one-time pip install, no configuration change — and the old directory is left on disk for you to delete.
dbt_verify— the dbt engineer’s verify-by-execution tool. Runs a dbt subcommand through the component’s pinned engine and renders per-model results, so the agent sees which node failed and why rather than a wall of dbt log.dbt_adapter_versionon[components.dbt], so the warehouse adapter can be pinned independently of the dbt-core version. Omit it and behaviour is byte-identical to before.- Reviewer telemetry. Each reviewer invocation now lands an
agent_invocationsrow with its tokens and cost, sovalved metrics agentsandvalved metrics costssee the review fan-out for the first time. failed_by_engineon a routed design, recording which sub-goal was dropped, so a partial plan is honest about the gap. A blocked engine and its question are recorded the same way.- A warning when a delegated agent gets no warehouse target on a project that has configured warehouse targets — previously a silent early return.
- The scaffolded
runner.tomlnow documents the[plausibility]gate that shipped in 0.2.1, alongside the already-documented[auto_fix]block. See load plausibility.
Highlights; the full list is in the changelog.
valved connectcan provision dbt for a production warehouse. Engine resolution routed snowflake/bigquery/databricks/redshift to an engine whose installer cannot yet materialize, so every dialect that matters was oneconnectcould not install. Resolution now only honours a preference the installer can actually build, falling back to dbt-core.- The dbt adapter is no longer pinned to the dbt-core version. The requirement was built as
dbt-<adapter>==<engine version>, assuming core and adapters release in patch lockstep. They do not, so a valid engine pin died onNo matching distribution found. - Every delegated agent’s
sqltool now reaches the project’s real warehouse — engineers, reviewers, the explorer behindvalved ask, and recovery’s fix proposer all designed against an empty database while believing they had queried Snowflake. The SQL dialect follows the connection too, rather than being hardcoded to DuckDB. - A build no longer records a
Buildfor files the reviewers never saw, and a build can recognise work a previous build already wrote — a build that authored files but died before recording its row previously left the plan permanently unbuildable. - One engine’s failure no longer discards every other engine’s work, and a routed design is no longer thrown away when an engineer asks a question.
valved buildnow refuses a plan that still records a blocking question — writing nothing, leaving itdrafted— and prints the engineer’s actual question. valved buildno longer invents a cause for a failed build. It printed “Build did not write amain.py” for every unsuccessful build, which is only ever true of the single-engine extract-load path.valved init --with-dbtscaffolds a project that builds. The staging model selected a source relation nothing in init created, so the firstdbt buildin a brand-new project failed outright. It now ships a seed the staging model references.- Fresh projects are no longer shipped invented standards.
standards.mdscaffolded worked examples stated as fact — “Stripe data must always be loaded incrementally”, in projects with no Stripe — and the memory selector puts standards into every agent invocation. The guidance stays; every rule-stated-as-fact is gone. - The plan agent can enumerate the filesystem. It had
read_filebut nogloborgrep, so finding a file meant guessing a path. Both new tools are confined to the project directory and skip secret files by construction. - Two dbt components differing only in dialect no longer share one venv.
- The docs site documented configuration the loader rejects. The
connections.tomlreference showed a shape that was never shipped,valved.tomlexamples carried a key the loader rejects, and theruntime.tomlreference listed five blocks that all raiseConfigError.
Earlier releases
Section titled “Earlier releases”0.2.1, 0.2.0, 0.1.1 and 0.1.0 are documented in CHANGELOG.md.