Skip to content

REST API

Valved is headless by default. valved serve exposes a FastAPI REST API covering the core loop (plan, build, deploy, run) and the control plane (jobs, workers, pipelines, schedules, tokens, webhooks, metrics, memory), so an external agent, chat tool, or script can drive Valved without the CLI.

It is not a mirror of the CLI. Project setup — init, connect, auth, creating or changing targets, graduating a component, registering an MCP server, el run — has no REST equivalent, and the routers for targets, components, agents, skills and MCP servers are read-only. POST /plans accepts only {goal, pipeline_name}, so plan --refine and the destination pre-seeds have no endpoint either.

valved serve [--workers N] [--interval S]
# binds from [api] host/port in valved/api.toml — default http://127.0.0.1:8765

The OpenAPI schema is served live, and there’s interactive Swagger:

Terminal window
curl -s http://127.0.0.1:8765/api/openapi.json # machine-readable schema
open http://127.0.0.1:8765/api/docs # Swagger UI

The MCP server is generated from exactly this schema, so the REST surface and the MCP tool catalog are always in lockstep.

Terminal window
curl -s http://127.0.0.1:8765/healthz # liveness
curl -s http://127.0.0.1:8765/readyz # readiness (200 once migrations are at head)

The API is organized into routers under /api/v1, mirroring the CLI groups:

Router Resource
plans draft plans (create, refine, show)
builds build runs against a plan
pipelines pipeline definitions + lineage
runs on-demand runs, logs, and the event stream
schedules live schedule (pause/resume/set-cron), audited changes
deploys promote built code via the configurable handoff
jobs / workers the queue and worker pool
asks / investigations read-only Q&A and recovery records
components / targets component references and connection targets
agents / skills / mcp_servers extensibility
memory / metrics project memory and cost/usage rollups
tokens / webhooks API token lifecycle and run-completion webhooks

Requests carry a bearer token. valved serve bootstraps one to .valved/token on first run; mint or rotate with:

Terminal window
valved auth rotate # prints the new token once
# then:
curl -s http://127.0.0.1:8765/api/v1/pipelines \
-H "Authorization: Bearer $VALVED_API_TOKEN"

Programmatic token lifecycle is REST-native: POST /api/v1/tokens and DELETE /api/v1/tokens/{id}.

Three endpoints back the explorer’s read-only Q&A: GET /api/v1/asks (cursor-paginated history, filterable by ?pipeline= and ?since=), GET /api/v1/asks/{ask_id} (one ask, with its full citations hydrated from the durable trace), and POST /api/v1/asks, which runs the explorer synchronously and returns the completed ask — 5–30 seconds is typical.

POST /api/v1/asks takes three body fields:

Field Type Required Meaning
question string yes The question to answer.
pipeline string no Scope the investigation to one pipeline.
target string no The target whose warehouse connection backs the explorer’s sql tool.
Terminal window
curl -s "$BASE/asks" -H "$AUTH" -H 'Content-Type: application/json' \
-d '{"question":"what breaks if I change raw_stripe.charges?"}'

target is not just a label on the ask row — it is resolved into the connection the explorer actually queries, so an ask over REST reaches your warehouse exactly as valved ask does (see the core loop). Because it now reaches target resolution, it is validated against the target-name regex ^[a-z][a-z0-9_]*$: a free-form label that earlier releases would have stored returns 400 with type https://valved.dev/errors/target-resolution. Values that follow the normal target-name rules are unaffected. Omit the field and Valved resolves VALVED_TARGET, then default_target — an unscoped ask still queries the project’s default target.

A failed ask is not an HTTP error. If the explorer cannot answer, POST /api/v1/asks still returns 200 with "status": "failed" and an error_message — check the body’s status, not the HTTP code.

Every error is RFC 9457 application/problem+json: {type, title, status, detail, instance} plus endpoint-specific fields. type is https://valved.dev/errors/<slug>; switch on the slug, never on title or detail. The slugs you’ll meet on the asks endpoints:

Status Slug When
400 target-resolution target fails the target-name regex (POST /asks).
400 bad-request A malformed ?since= window on GET /asks.
401 missing-bearer-token / invalid-token No Authorization: Bearer header, or an unrecognized token.
404 not-found GET /asks/{ask_id} for an unknown or non-UUID id.
422 validation Body failed schema validation — e.g. question omitted.

Other routers surface their own slugs the same way — POST /builds returns 409 config-drift (with plan_id, expected_config_hash, actual_config_hash and a recovery_hint in the body) or 409 plan-expired; POST /plans returns 422 plan-generation. An unrecognized internal failure becomes a 500 internal with an opaque detail; the stack trace is logged server-side, never returned.

  • Run event streamGET /api/v1/runs/{run_id}/stream streams live run events (this is what valved run --watch consumes). Because it’s a stream, it is not exposed as an MCP tool.
  • Webhooks — register a webhook to receive run-completion callbacks; manage them under /api/v1/webhooks (create, patch, rotate-secret, delete).
Terminal window
BASE=http://127.0.0.1:8765/api/v1
AUTH="Authorization: Bearer $VALVED_API_TOKEN"
PLAN=$(curl -s "$BASE/plans" -H "$AUTH" -H 'Content-Type: application/json' \
-d '{"goal":"ingest the Stripe charges API into raw_stripe"}' | jq -r .id)
curl -s "$BASE/builds" -H "$AUTH" -H 'Content-Type: application/json' \
-d "{\"plan_id\":\"$PLAN\"}"
curl -s "$BASE/runs" -H "$AUTH" -H 'Content-Type: application/json' \
-d '{"pipeline":"stripe_charges"}'