CLI reference
Every command supports --help. Global flags precede the subcommand.
Global flags
Section titled “Global flags”valved [--project-dir PATH] [--env-file PATH] [--target NAME] <command>| Flag | Meaning |
|---|---|
--project-dir PATH |
Project root (dir containing valved.toml). Defaults to cwd. |
--env-file PATH |
.env path. Defaults to <project-dir>/.env. |
--target NAME |
Active target; overrides $VALVED_TARGET and default_target. |
--install-completion / --show-completion |
Typer’s shell-completion helpers. |
Target resolution is first-hit-wins: a subcommand’s own --target → the global --target → $VALVED_TARGET → default_target in valved.toml → dev. Many subcommands take their own --project-dir and --target; where both exist, the subcommand’s flag wins.
Set VALVED_NO_DOTENV=1 to disable .env auto-loading. There is no global --version — use valved version.
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
0 |
Success. |
1 |
The work ran and failed, or you aborted a confirmation prompt. Agent errors, a build that didn’t complete, a failed deploy. |
2 |
Refused before the work ran: invalid config (valved.toml won’t load), or bad state — plan not found, wrong phase, expired, blocked. |
3 |
build / plan-and-build: config drift (see below). init: environment failure — Docker not detected, or the external Postgres was unreachable. |
4 |
deploy pre-flight drift — the build’s config_hash diverges from current config. Re-plan and rebuild before deploying. |
There is no exit code 5. These verbs read the local state store and project directly; they never round-trip to a running server, so “server unreachable” is not an exit path.
The split between 1 and 2 is the useful one: 2 means Valved declined to start, 1 means it started and the work failed. build uses both — see its refusal table below.
Lifecycle
Section titled “Lifecycle”valved init [DIR] [--with-dbt] [--dbt-path P | --dbt-url U [--dbt-branch B]] [--with-dlt] [--dlt-path P | --dlt-url U [--dlt-branch B]] [--external-postgres URL] [--project-name N] [--default-target T] [--no-git-init] [--non-interactive] [--adopt-existing]valved connect [<component>] [--project-dir P] [--target T]valved plan <goal> [--refine <plan_id>] [--pipeline <name>] [--table T] [--database D] [--schema S] [-q]valved build <plan_id> [--force] [--table T] [--database D] [--schema S] [--max-turns N] [-y] [-q]valved plan-and-build <goal> [--table T] [--database D] [--schema S] [-q]valved run <pipeline> [--target T] [--watch]valved runs [--limit N] [--pipeline N] [--recovery <run_id>]valved logs <run_id> [-f]valved cancel <run_id>valved deploy <pipeline> [--handoff files|commit|push|pr] [--amend] [--draft] [--reconcile-pins] [--investigation <id>] [--target T] [--yes]--dbt-branch and --dlt-branch default to main; --default-target defaults to dev.
valved init names the database it initialized (… → host:port/db), and refuses — exit 2, nothing written — to migrate one that already holds a Valved state store. This matters most on the bundled path, where every project resolves to the same 127.0.0.1:5432/valved: without the check, running init for a second project while the first project’s Postgres is up would upgrade that project’s live schema, unprompted and unreported. Give the new project its own port with VALVED_POSTGRES_PORT (and a matching port in DATABASE_URL), or pass --adopt-existing to take the store over on purpose. Re-running init where a valved.toml already exists adopts without the flag.
valved connect
Section titled “valved connect”Provisions a dbt component’s engine on demand: resolve the engine and version from the target’s warehouse dialect, install it, validate it, then write the resolved pin back into valved.toml so it is reused rather than re-resolved. Idempotent and fail-closed — if validation fails, no pin is written.
With no argument it provisions the project’s single detected dbt component; naming a component uses its [components.<name>] block, or the convention-discovered dbt project. An ambiguous or absent dbt component is an error (exit 2).
Scope is narrower than the name suggests. This provisions the bundled dbt-core engine only. A managed backend (snowflake-native, dbt-cloud, remote) or an external dbt (dbt_env = "external") is wired but nothing is installed. Warehouse and source connection — credential capture, profiles.yml — is not part of this command.
--target selects the warehouse dialect that picks the engine; with no warehouse block for the active target it falls back to DuckDB.
valved plan
Section titled “valved plan”Turns intent into a reviewable plan; nothing is written to the working tree. It warns when the plan’s own runtime estimate nears runner.default_timeout_seconds. Plans expire 24 hours after creation.
--refine <plan_id> iterates on a draft, recording parent_plan_id on the new plan. The parent must still be in phase drafted — refining an already-built plan is refused and points you at valved plan --pipeline <name> instead.
A refine inherits its parent’s path. A routed parent — one whose design carries planned_by_engine — refines through the routed path: the same engineers, in the same order, on the same slices. Each is handed its own prior design, its own question if it asked one, and your feedback; the child plan comes back routed, so build still authors the right artifact kind. A parent with no recorded routing refines through the single-engine extract-load planner, unchanged. Routing is read off the parent, never re-derived from the feedback text, so the artifact kind cannot change under you in either direction.
Three other things the child inherits, so a refinement is never read as a standalone goal: the parent’s pipeline name (a refine does not rename the artifact — otherwise “make it hourly” would retarget stripe_models to make_it_hourly), the parent’s destination, which --table / --database / --schema still override — as does a fully-qualified name written into the feedback itself (“…into analytics.sales.iowa_sales” re-seeds table, exactly as it would on a fresh goal) — and a stored goal carrying the original ask plus your feedback. That composed goal is the string build later hands to both the engineer that authors the code and the review gate that judges it, so your feedback survives past plan time and the author is never told less than its reviewer.
--refine is therefore the answer to build’s blocked-plan refusal. When a routed engineer returns needs_user_input, build prints its question and exits 2; valved plan --refine <plan_id> "<your answer>" puts that answer in front of the engine that asked, and when every engineer is satisfied the block clears and the child builds.
A refine prints a field-by-field refinement diff afterwards. It covers the routing fields as well as the design ones, and it lists only what changed — so an answered question shows up as blocked_by_engine going to (unset). Note that planned_by_engine carries each engine’s proposed files as well as its slice, so it renders whenever a refine moved the file list — that is a design change, not a routing change; the engines and their order are inherited and cannot differ.
--pipeline <name> still does not route, and that is deliberate rather than an oversight: the routed path has no per-engine “here is your existing slice” context, and the context that does exist inlines el/<name>/main.py and is extract-load-shaped. Use it for a delta against an existing extract-load pipeline.
A routed refine that produces no design at all does not fall back to the single-engine planner — falling back there is what used to hand you an extract-load plan for a goal that had been routed to dbt. It exits 1 instead. The error quotes verbatim any question an engine asked instead of designing, and what any engine that outright failed said; when a question is what came back, it points you at answering it with --refine on the parent. (An engine that reports success while proposing no files is in neither group, so that rarer case exits 1 with the reason and remedy alone, and no per-engine detail.) The parent plan is untouched and stays drafted.
valved build
Section titled “valved build”Runs the build agent against a drafted plan and verifies by executing; output lands in the working tree.
By default it prints the destination FQN (database / schema / table, each tagged as an override or inherited from the target’s connection) and prompts y / e (edit) / n. -y skips the prompt; answering n aborts with exit 1. --table / --database / --schema override the plan’s design.destination before the agent runs.
--force covers exactly one thing: re-running the agent against a plan already in phase=built. It overrides the phase gate and opts out of the idempotent no-op (rebuilding an already-built plan whose recorded files are still on disk otherwise returns the existing build without re-running the agent). It does not bypass any of the other refusals:
| Refusal | Exit | --force helps? |
|---|---|---|
Config drift — the plan’s config_hash no longer matches current config |
3 |
No. Re-plan against current config. |
| Plan expired (24 h after creation) | 2 |
No. Re-plan. |
Blocked — a routed engineer returned needs_user_input, so its design is unfinished |
2 |
No. Answer the question: valved plan --refine <plan_id> "<answer>", then build the child plan. |
Plan is in a phase other than drafted |
2 |
Yes — this is what --force is for. |
| Plan id not found | 2 |
No. |
| Agent ran but did not complete | 1 |
n/a — plan stays drafted; refine and retry. |
The gates run in that order, so drift and expiry are checked before --force is ever consulted.
--max-turns caps each agent in the build, defaulting to [runner.agent] max_turns in valved/runner.toml. It is a runaway-loop backstop, not a spend cap: a healthy build costs the same whatever the ceiling is, because it stops when the work is done. Raising it does not make a stuck agent finish — if a build exhausts the ceiling, the error names the agent that stalled and lists any files it had already written, and that is the thing to read rather than the number to raise.
--database and --schema are refused on a routed (multi-engine) plan, exit 2, before any agent runs. That path writes no destination.toml, and a dlt load lands in the schema named by its component’s dataset — which the plan chooses — so the flags could not confine where data goes. Set the destination in the plan, or point the target’s <TARGET>_SNOWFLAKE_DATABASE / _SCHEMA. On a single-engine plan they work as described above.
On success it prints a next step matched to what was built: valved run for a full pipeline (a pipelines/<name>.toml exists), valved el run for an EL-only component.
valved plan-and-build
Section titled “valved plan-and-build”Plans then builds in one step. Note what it does not have: no --force, and no -y — it never shows the destination confirmation prompt that valved build does. The destination flags are passed straight through to the plan stage. It uses the same drift (3) and plan-state (2) exits as build.
The rest
Section titled “The rest”runslists recent runs (--limitdefaults to 20); a run whose load tripped the plausibility gate’s warn threshold carries a⚠ suspectbadge.--recovery <run_id>renders that run’s recovery-attempt chain as a tree.logs <run_id>prints a run’s log lines;-f/--followkeeps printing until the run finishes (Ctrl-C to stop).cancel <run_id>stops a run that has not finished.deploypromotes built code via a configurable handoff (defaultpr), with linked PRs across repos.
Investigate
Section titled “Investigate”valved ask <question> [--pipeline N] [--target T] [--output text|json] [--watch/--no-watch]valved asks list [--since <dur>] [--pipeline N] [--limit N]valved asks show <ask_id> [--output text|json] [--include-trace]valved investigations list [--status proposed|acknowledged|resolved|dismissed] [--since <dur>] [--pipeline N] [--limit N]valved investigations show <id> [--all-runs]valved investigations dismiss <id> --reason "<text>"--watch is on by default in a TTY. asks list and investigations list default to --since 7d and --limit 50; investigations show caps recurring runs at 10 unless you pass --all-runs.
valved ask --target selects the warehouse
Section titled “valved ask --target selects the warehouse”--target used to be a label on the stored ask row. It now resolves the active target whose connection backs the explorer’s sql grant — the warehouse the answer is actually grounded in. It resolves the same way every other command’s does: --target → $VALVED_TARGET → default_target in valved.toml.
This is worth knowing because of what happens when no warehouse target resolves: the explorer falls back to a credentials-free in-memory DuckDB, whose introspections all succeed and return nothing. The explorer can then answer a warehouse question from an empty database. Valved warns in that case when the project does have warehouse targets, but the failure mode is quiet enough to be worth naming — if an ask about your warehouse comes back oddly empty, check which target it resolved.
Serving
Section titled “Serving”valved serve [--workers N] [--interval S] [--reaper-interval S] [--archive-interval S] [--no-archiver] [--drain-timeout S] [--label L] [--no-ui-regen]valved worker [--once] [--workers N] [--poll-interval S] [--label L]valved mcp-serve [--transport stdio|http] [--port N] [--host H] [--server-url URL] [--token T] [--log-level LEVEL]serve runs FastAPI + scheduler + reaper + archiver + worker pool. It takes no --host/--port — the REST server binds from [api] host/port in valved/api.toml, defaulting to 127.0.0.1:8765. There are no --no-scheduler / --no-reaper switches either; only the archiver can be skipped (--no-archiver). Defaults: --workers 1, --interval 30, --reaper-interval 30, --archive-interval 3600, --drain-timeout 300 (aliased --grace-period). Ctrl-C / SIGTERM drains in-flight jobs within the drain timeout; a second signal cancels immediately.
--label on serve and worker advertises a worker-placement label. A labeled worker claims matching labeled jobs plus unlabeled ones; without a label it claims only unlabeled jobs. worker --once claims and runs a single queued job, then exits.
For mcp-serve, --transport defaults to stdio (ws is a deprecated alias for http), --port to 8766, --server-url to http://127.0.0.1:8765. Token discovery: --token → $VALVED_API_TOKEN → .valved/token.
Schedule (live data — no deploy)
Section titled “Schedule (live data — no deploy)”valved schedule listvalved schedule show <pipeline>valved schedule pause <pipeline> [--reason "<text>"]valved schedule resume <pipeline> [--reason "<text>"]valved schedule set-cron <pipeline> "<cron>" [--timezone TZ] [--target-pipeline N] [--reason "<text>"]Changes take effect within one scheduler loop interval and are audited. --target-pipeline sets the target for a newly created schedule (default prod).
Pipelines, EL, components, targets
Section titled “Pipelines, EL, components, targets”valved pipelines list [--project-dir P]valved pipelines show <name>valved pipelines validate [<name>]
valved el run <name> [--target T] [--watch] [--no-auto-fix] [--max-fix-attempts N]valved el list [--target T]valved el verify <name> --target T [--no-smoke-test]
valved component <name> --separate-remote <url> [--ref <pin> | --branch <name>]valved component <name> --separate-local <path>valved component <name> --same-repovalved components show [<name>]
valved target create <name> [--force]valved target listvalved target show <name>valved target rename <old> <new>valved target delete <name> [-y] [--force --no-default-warning]el verify --target is required, unlike every other --target in the CLI. el run --watch re-runs on filesystem changes under the artifact directory (it does not stream a remote run, as valved run --watch does); --max-fix-attempts overrides runner.toml’s [auto_fix] max_attempts. Deleting the default target needs both --force and --no-default-warning.
Extensibility
Section titled “Extensibility”valved agents list|show <name>valved agents create <name> [--template <existing>]valved skills list|show <name>valved skills test <name>valved mcp-servers listvalved mcp-servers add <name> --command "<cmd>" | --url <url>valved mcp-servers remove <name>skills test takes no arguments beyond the pack name — it renders exactly what would be injected, rather than executing the skill. A user agent at valved/agents/<name>.md overrides a built-in of the same name.
valved memory show [<kind>] [--pipeline N] [--el N]valved memory edit [<kind>] [--pipeline N] [--el N]valved memory append-decision "<title>" [--body "<text>"] [--reviewers a@,b@] [--date YYYY-MM-DD] [--force]valved memory refresh
valved metrics costs|runs|agents [--since <dur>]
valved docs serve [--host H] [--port N] [--watch/--no-watch] [--no-auto-regen]valved docs regen [--page <name>]valved docs open [--host H] [--port N]
valved auth status # model-provider auth mode (never a secret)valved auth login # mint a Claude-subscription OAuth tokenvalved auth rotate # mint a new REST/MCP API bearer token → .valved/token
valved versionmemory edit opens the file in $EDITOR and writes it directly — there is no --direct flag, and no review-mediated alternative to opt out of. memory refresh infers the dbt project’s conventions and writes conventions.md; it takes no --backend and does not read dlt. metrics windows default to --since 7d. docs serve defaults to 127.0.0.1:8766 and watches for new state-store events unless you pass --no-watch.
Every command in this section also accepts --project-dir.
Not implemented
Section titled “Not implemented”These are registered and appear in --help, but print a message and exit 1:
| Command | Why |
|---|---|
valved pipelines diff <name> --against <build_id> |
plan/build do not populate a per-pipeline build manifest yet, so there is nothing to diff against. |
valved schedule reseed <pipeline> |
The live schedules table is owned by the runtime; re-applying [seed_schedule] from code is not wired up. |
valved schedule reseed is the one to watch for. A pipeline’s [seed_schedule] block is parsed and validated, but nothing writes it into the schedules table — the reconciler that would seed it at registration has not shipped, and reseed is the command that would re-apply an edited block. So a [seed_schedule] block has no path to taking effect at all today, on a new pipeline or an existing one. Create and change live schedules with valved schedule set-cron, which upserts.
Environment variables
Section titled “Environment variables”| Var | Purpose |
|---|---|
DATABASE_URL |
Postgres connection string for the state store. |
ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN |
Model-provider credentials (never passed into the sandboxed bash tool). |
VALVED_API_TOKEN |
Bearer token for REST/MCP. |
VALVED_TARGET |
Default active target. |
VALVED_NO_DOTENV |
Set to 1 to disable .env auto-loading. |