Skip to content

Server API

barca serve starts an HTTP server that runs assets and tasks on request, fires Schedule(...) nodes on their cron, and serves a web UI at /ui/.

The server binds to 127.0.0.1 and has no authentication: anyone who can reach the port can start and cancel runs. To reach it from another machine, put a reverse proxy that authenticates in front of it (see Deploying).

Scope: this is the implemented barca serve API, including the run contract and inspection routes of the server-mode RFC (section 3.4), and its ledger (#402). The CLI is a client of these routes: barca get daily_report --server <url> runs on the server and prints what a local run prints (Deploying), and with a server configured ([server] url or BARCA_SERVER) a laptop’s barca get reads and posts to the ledger routes over http:// or https://.

Runs execute inside the server process, in the background. POST /runs answers at once with the run’s id, which is its database run id (the one barca history shows); the run’s events stream from GET /runs/{run_id}/events, and GET /runs/{run_id} answers from .barca/metadata.db once it has finished, so the answer survives a restart.

Terminal window
barca serve pipeline.py # one file, default port 8274
barca serve pipeline.py --host 0.0.0.0 # every interface (containers, VMs)
barca serve --port 8400 --timezone utc # every file in the project

The flags (--host, --port, --watch, --no-schedule, --timezone, --read-only, --drain-timeout, --env) are in the CLI reference. On start the server prints its address and the schedule on stderr:

[barca] serving on http://127.0.0.1:8274 (1 file)
[barca] scheduling 1 asset:
pipeline.py:daily — 0 5 * * * (next 2026-10-08 05:00:00)

The default bind address is 127.0.0.1. --host takes an IP address, including :: for IPv6; hostnames are rejected. 0.0.0.0 listens on every IPv4 interface. There is no authentication, so anyone who can reach the port can trigger runs. A non-loopback address prints a startup warning; use a private network or an authenticating proxy.

--watch re-parses the DAG when a source file changes, so /assets and /plan reflect edits without a restart. Files added after the server started are not picked up until a restart, with or without --watch.

With a remote store configured, barca serve backs its metadata DB up to the store every minute it changed. See Deploying.

The normative OpenAPI 3.1 contract defines the current routes, bodies and statuses and is checked against the real router and Python Client. This page explains that contract; it does not advertise proposed RFC routes.

Responses are JSON, except the event stream, a run’s output and the UI. Every error body is the CLI’s error envelope (see Errors). Axum request-extractor errors (for example, an invalid /runs?limit=) can instead return 400 plain text. GET routes also accept HEAD and return the corresponding headers without a body.

Runs.

Method Path Description
POST /runs Start a run of barca get or barca run on the server. Body: RunRequest. 202 {"run_id", "status", "position"} (the run joins the run queue: running when it started at once, or pending and its place, 1 = next), or 200 with the --dry-run JSON when dry_run is set.
GET /runs/{run_id} The run: what started it, its status and, once finished, its outcome, with its steps and captured lines. Survives a restart. At most 1000 steps (?limit=N, ?all=true; failed steps first), with steps_page: {total, truncated}.
GET /runs/{run_id}/events Server-sent events, each with an id:: run_started, line, log, step_finished, run_finished.
DELETE /runs/{run_id} Cancel a pending or running run.
GET /runs/{run_id}/output The final artifact’s bytes of a successful single-target run.
GET /runs/{run_id}/logs What the run’s steps wrote (stdout and stderr), so far while it runs: {run_id, status, logs}. ?step= and ?stream=stdout|stderr filter as barca logs does.
GET /runs?limit=100 Recent durable runs, joined with queued and live server runs.
GET /logs/{run_id} A run’s captured output lines, persisted after it finishes.

The ledger. What a local barca get on another machine talks to (barca docs server-mode).

Method Path Description
POST /ledger/lookup Body {"hashes": [{"node_id", "run_hash"}]}. Answer {"results": [...]}, one per pair in order: {"node_id", "run_hash", "exists": true, "path", "format", "size_bytes", "digest"} for the latest successful result the ledger holds, or exists: false. By run hash only.
POST /ledger/runs Body: one finished run, {"run": {run_id, command, files, target, status, steps_*, started_at, finished_at, elapsed_seconds, host}, "steps": [{node_id, run_hash, status, path, format, size_bytes, digest, ...}]}. Every successful step must name an object in this server’s store with its SHA-256 digest; the server checks each (a directory store by digest, an object store by size), then records the run as triggered_by: client with client_run_id and answers 201 {"run_id", "bytes_changed"}. A missing, foreign or mismatched object is 409 with the envelope naming the node, and nothing is recorded. A run hash the ledger already holds with the same digest adds nothing; with another digest the ledger keeps its row and lists the step in bytes_changed. A repeated post of the same run.run_id answers with the run already recorded. 400 without a store, 403 read-only, 503 while shutting down.

Inspection. The body is the CLI command’s --json output, built by the same function, so a client prints it as it would print the local command’s.

Method Path Same as
GET /list barca list --json --all
GET /nodes?targets=a,b&sample=N barca status --json --all, for the server’s ledger
GET /history?limit=N&all=1 barca history --json (-l N, --all)
GET /stats/{name} barca stats --json
GET /sql/views?files=a.py,b.py Which result each barca sql view reads in the ledger: {"views": [{name, node, kind, partitioned, format, files: [{path, partition, content_hash}], stale, run_hash}], "missing": [{name, node, kind, partitioned, command, keys_unknown?}], "duplicates": [{name, ids}]}, by the function a local barca sql uses. The client runs the query; the server runs no SQL.
GET /materializations?limit=N&offset=M Step results, newest first: {"materializations": [{node_id, partition, run_id, run_hash, status, created_at, elapsed_seconds, cpu_seconds, max_rss_bytes, attempts, format, size_bytes, artifact, content_hash, error_type, error_message}], total, limit, offset}; limit defaults to 1000, at most 10000. barca sql’s barca.materializations.
GET /project The deployed tree: {api_version, root, env, deployed_at, commit, files: [{path, sha256}]}
GET /health Liveness (or draining), api_version, env, the artifact store (store_uri), read-only, scheduler, load diagnostics, the cache size against its limit, started_at (changes on a restart) and queue: {paused, queued, running, max_runs, capacity} (capacity: {total, in_use, waiting} with --capacity, else null).

The served graph and the UI.

Method Path Description
GET /state Every node: its barca status entry plus typical durations, next run and the run working on it now.
POST /state/reset Start the metadata database over, as barca state reset: it moves aside to metadata.db.reset-<UTC>, nothing is deleted. See Resetting the database.
GET /groups Organizational groups as [{id, name, description, members, output}]; separate from execution edges.
GET /assets Every node as barca list --json shows it.
GET /assets/{name} One node’s barca list --json entry with its barca stats --json body and the run working on it now.
GET /assets/{name}/schema Selected node and direct inputs as NodeStatus[], with artifact shapes inspected on demand.
GET /plan Execution plan (phases and streams) as JSON.
GET /schedule Scheduled jobs with next fire time, last run, last skipped tick and pause.
POST /schedule/{id}/pause Hold one schedule back, optionally ?until=<RFC 3339>.
POST /schedule/{id}/resume Let a paused schedule fire again.
GET /queue The run queue: {paused, paused_at, draining, max_runs, running: [...], queued: [...], capacity}, each run {run_id, status, position, command, target, triggered_by, schedule_id, queued_at}; capacity is {total, source, in_use, runs: [{run_id, weight}], waiting: [{run_id, node, weight, waiting_seconds}]} with --capacity, else null.
POST /queue/pause Hold every queued run back until resumed; runs in flight go on. Kept across restarts. Answers the queue; 403 read-only.
POST /queue/resume Let queued runs start again, in order. Answers the queue; 403 read-only.
GET /, /ui Redirect (relative Location: ui/) to the web UI.
GET /ui/ The web UI, compiled into the binary.

Older routes, retained for Python Client compatibility. They run on the same functions as the routes above, and the run_id they answer with is the database run id. New integrations should use /runs. The aliases remain deprecated, with no scheduled removal; removing them requires migrating Client first and announcing the breaking change in a minor release.

Method Path Use instead
POST /run POST /runs with {"command": "get"}
POST /run/{target} POST /runs with {"command": "run", "targets": [...], "refresh_all": true} (the alias recomputes upstream assets)
POST /get/{target} POST /runs with {"command": "get", "targets": [...]}
DELETE /run/{run_id} DELETE /runs/{run_id}
GET /status/{run_id} GET /runs/{run_id}
GET /events/{run_id} GET /runs/{run_id}/events
POST /runs
Content-Type: application/json
{
"command": "get", // "get" | "run"
"targets": ["daily_report"], // as given on the command line; [] means every asset and sensor
"files": [], // deployed files, as typed relative to the root; the run reads only these. [] means all of them
"refresh": [], "refresh_all": false, "cascade": true,
"dry_run": false,
"batch": ["scored=1000"], // optional --batch overrides for nodes declaring batch=
"agent": true // which stderr lines the client will print
}

A partitioned run returns every key’s value in outcome.final_output, keyed by partition key, with tables as JSON rows. The same bounded summary as get applies above 1000 keys.

Only command is required. The fields are the flags of barca get and barca run (--refresh, --refresh-all, --no-cascade is "cascade": false, --dry-run, --agent). The optional batch array takes repeatable --batch values (all, none, a positive integer, or NODE=VALUE). It overrides grouping only for nodes that declare batch=. Invalid values or names are 400 before a run starts. A dry run adds {grouping, keys, calls} to each affected step.

The command runs on the server with its deployed code. A file that is not one of the deployed files is a 400 naming them.

{ "run_id": "1b942bb33182" }

The targets are checked before anything starts, with the function barca get and barca run use, in the CLI’s words: an unknown name is 404 (with the barca list remediation), a name that matches several nodes 409, a target of the wrong kind (get of a task) 400, and run without a target 400. No run is started for a refused request and there is no run_id. dry_run answers 200 at once with the --dry-run --json body, decided against a private copy of the metadata DB; nothing starts and nothing is written, also on a --read-only server. A read-only server refuses every other run with 403.

The check is made against the source as it is now: the server keeps the list of nodes between requests and reads the files again when one of them has changed size, modification time or change time. A run that was started can still fail on its target if the source changes between the check and the run; its outcome is then an error with the CLI’s envelope.

Every run is one node: it executes in worker processes on the server’s machine, bounded by a run pool sized to the machine’s CPUs, and is written to .barca/metadata.db like a run from the command line, with triggered_by: "api". A run that exceeds the server’s 10-minute limit is stopped and ends with an error outcome.

GET /runs/{run_id}/events → text/event-stream

Each event is a server-sent event whose data: is JSON with a type, and whose id: is its number in the run, from 0:

type Payload Meaning
run_started run_id The run left the queue.
line line, when A stderr line exactly as a local run prints it: --agent step lines (when: "agent"), warnings, [barca] notes and the end-of-run line (when: "always"). A client prints the lines its own mode would.
log node_id, line A line a step printed.
step_finished node_id, ok, cached?, elapsed_seconds?, error?, import_seconds? A step ran, failed, or was served from cache (cached: true).
run_finished run_id, ok, outcome The run’s outcome. The stream ends after it.

A subscriber receives the run from its first event whenever it connects, and every event once, however slowly it reads: the server keeps the run’s events for as long as it keeps the run (an hour after it finished, while finished runs hold at most 64 MB of memory; past that the oldest go first, never the run that finished last) and a lagging subscriber reads on from where it was. A connection that drops mid-run is not resumed: reconnecting replays the run from its first event, and resuming from Last-Event-ID is future work. The run goes on whether anyone is reading. Once the run is no longer kept, the stream is a 404; GET /runs/{run_id} answers from the database then.

The response carries X-Accel-Buffering: no so nginx passes events through as they happen, and a keep-alive comment every 15 seconds.

GET /runs/{run_id}
{
"run_id": "1b942bb33182",
"status": "success",
"command": "get",
"target": "daily_report",
"files": ["pipeline.py"],
"triggered_by": "api",
"scheduled_at": null,
"schedule_id": null,
"started_at": "2026-10-20T05:00:00.000Z",
"finished_at": "2026-10-20T05:00:01.000Z",
"elapsed_seconds": 1.2,
"steps_total": 2, "steps_executed": 1, "steps_cached": 1,
"outcome": { "status": "success", "run_id": "1b942bb33182", "final_output": {"_barca_artifact": {"path": ".barca/default/artifacts/pipeline.py:daily_report/9f2c.json", "format": "json", "size_bytes": 8}}, "steps": [], "...": "..." },
"error": null,
"steps": [{ "node_id": "pipeline.py:daily_report", "status": "success", "elapsed_seconds": 0.4, "error": null }],
"logs": [{ "node_id": "pipeline.py:daily_report", "seq": 0, "line": "wrote 3 rows", "stream": "stdout", "ts": "2026-10-20T05:00:00.512Z" }]
}

status is pending or running while the run is live, then the outcome’s status; a row the server did not start reads as the database has it (success, failed, cancelled, interrupted). triggered_by is api for a run started over HTTP, schedule for a cron tick (with the tick in scheduled_at, RFC 3339 in UTC, and the scheduled node in schedule_id), manual for a command typed on a machine, and client for a run a laptop posted to the ledger (#402); it is null for runs recorded before barca recorded it.

outcome is null while the run runs, and for runs not started by barca serve. Otherwise it is tagged by status:

  • success: the exact barca get --json / barca run --json stdout body. For get, final_output is the artifact pointer ({"_barca_artifact": {"path", "format", "size_bytes"}}, a path on the server), and for a partitioned target an object of them keyed by partition key, as barca get --json prints it (past 1000 keys, the _barca_partitions summary that lists only the keys the run computed); for run it is the value for a JSON artifact and an artifact pointer for binary formats. Several targets carry targets, in the order given.
  • failed: the CLI’s failed-run stdout body (failed_node, error, steps, …) plus envelope, the stderr error envelope with the traceback.
  • cancelled, error: {"status", "envelope"} only.

The outcome is stored with the run’s row (runs.outcome_json), so GET /runs/{run_id} gives the same answer after the server restarts. A run cancelled while it was still queued, or refused by a source change after it was accepted, has a row of its own with its outcome.

GET /runs/{run_id}/output

The bytes of a successful single-target run’s final artifact, with Content-Type by format: application/json, application/vnd.apache.parquet, text/csv, and application/octet-stream for pickle. A run of several targets or of every asset, a failed run and an unknown run are 404; a run still going is 409. It reads the server’s local artifact.

DELETE /runs/{run_id}

Cancels a pending or running run. Its Python workers are terminated, the results of steps that had already finished are kept, and the run ends cancelled (in its events, in GET /runs/{run_id} and in barca history). The response is { "run_id": "...", "status": "cancelling" }. Cancelling a run that already finished returns 409, and an unknown id 404.

GET /runs?limit=100 returns { "runs": [...], "total": 12, "truncated": false }, newest first. limit defaults to 100 and is bounded to 1–1000. It includes runs from the CLI, HTTP and the scheduler in this environment’s local metadata DB, plus queued server runs. There is no source filter: a historical run remains visible even when its pipeline is no longer served.

GET /runs?status=running lists only the runs in flight, newest first: this server’s pending and running runs, and the barca get / barca run commands running on the same .barca/ whose process is alive (the check the scheduler uses to skip a tick). Each has active_steps, the nodes it is working on now as [{node_id, step, partitions}] (see running under State). Any other status is a 400.

Each summary has id and run_id (the database run id), handle (the same id while the server keeps the run in memory, null otherwise), command, files, target, status, triggered_by, scheduled_at, schedule_id, UTC started_at and finished_at, elapsed_seconds, steps_total, steps_executed, steps_cached, and error; with ?status=running, also active_steps.

The UI’s Runs view lists this history and shows what started each run; each run opens its status, timing, steps, errors and logs.

These inspection endpoints always read a private DB snapshot. They do not create or migrate the project’s database, change run status, or execute Python, including on a read-only server.

/list, /nodes, /history and /stats/{name} answer with the --json body of barca list --all, barca status --all, barca history and barca stats, byte for byte: the server builds them with the same functions. A --server client applies --limit and --fields itself. /nodes reads the server’s ledger; root is the server’s project root. next_fire in /list is in the server machine’s local time, as barca list prints it there.

GET /project describes the deployed tree: api_version, root, env, deployed_at (when the server started), commit (read from .git at the root, null outside a git checkout) and each served file with its sha256, hashed on each request so an edit shows at once.

GET /health
{ "status": "ok", "version": "0.27.0", "api_version": 1, "env": "default", "store_uri": null,
"read_only": false, "scheduler": true, "scheduler_enabled": true, "metadata_error": null,
"load_errors": [], "cache": { "size_bytes": 1830000000, "limit_bytes": 20000000000 } }

status is ok, or draining from the first SIGTERM or SIGINT until the server exits (see the drain); the response is 200 either way.

api_version is the major version of this API (1); a client refuses another, and additive fields do not bump it. env is the environment the server runs in. store_uri is the artifact store the server writes to (null when artifacts stay on its machine), so a client can refuse a mismatched [remote].

cache (from 0.25, experimental) is .barca/ against its size limit as the server last measured it, after a run or on its once-a-minute check (see Cache size); null before the first measurement, with [cache].max_size off, and on a --read-only server, which deletes nothing. A run result gains cache: {size_bytes, limit_bytes, evicted_files, evicted_bytes} when the check after it found .barca/ over its limit.

scheduler is true when this server fires Schedule(...) nodes: on by default, false with --no-schedule or --read-only. scheduler_enabled (from 0.28) is the same, and false once the scheduler stopped on an error; metadata_error then says why.

metadata_error (from 0.28) is why the metadata DB cannot be used, while it cannot, and null otherwise. A writer server sets its database up before it binds, and exits 3 when it cannot, so on a writer it is set only when the scheduler stopped on a database error later. A --read-only server whose database is unusable (empty, not SQLite, a newer format, a malformed schema) keeps serving and reports it here; it goes back to null once a read succeeds again.

load_errors is an additive array of {file, error, affected_nodes}. file names an unloaded source, error describes the failure, and affected_nodes names the actual excluded node IDs (empty if syntax failure prevents identifying definitions). Healthy source/definitions and their schedules remain available; unrelated nodes in a valid file with a blocked dependent are retained. /state keeps its array shape and contains only loaded nodes, with no synthetic error rows. These are source/graph diagnostics; arbitrary Python import/execution failures remain run errors. --watch repairs and removals refresh source diagnostics and scheduling. A run of every asset refuses an empty loaded asset/sensor set with 400; excluded targets are 404. The UI displays these diagnostics above the normal healthy graph.

GET /state → [NodeState, ...] (dependency order)
GET /state?targets=a,b → [NodeState, ...] (only those targets and their upstream dependencies)

targets is experimental: comma-separated names or full node ids. Absent or empty means every node. Unknown targets answer 404; ambiguous names answer 409. Status and duration queries are limited to the selected cone; clients can render /assets and /groups before requesting it.

Each NodeState is a barca status --json node (id, name, kind, inputs, partitioned, cache, partitions, last_materialization, shape, env) plus durations — { median_seconds, p95_seconds, samples } over the last 20 successful runs, or null — and next_run, the next cron fire time in unix seconds for scheduled nodes. cache is the barca status cache entry: state is cached, unsynced, stale, never_run, partial, unknown or always_runs, with a machine-readable reason and a detail in words (see barca status). shape is always null here: reading artifact shapes starts a reader process, too slow for an endpoint the UI polls. The cache check uses read-only connections to a compatible WAL metadata DB, or a private copy when migration is needed; this endpoint never writes the original rows.

running is the run working on the node now, whoever started it, or null:

"running": {
"run_id": "0eeee4d342ed",
"triggered_by": "schedule", // schedule, api, client, manual (a CLI run), or null
"started_at": "2026-10-21T10:00:00.000Z",
"step": "running", // or "queued"
"partitions": { "running": 6, "done": 4, "total": 10 } // null for an unpartitioned node
}

It covers this server’s runs (the scheduler’s, POST /runs, barca get --server) and the barca get / barca run commands on the same .barca/ whose process is alive. A server run covers its targets and the nodes upstream of them; a node is queued while the run waits for a run slot or for the node’s upstream steps, and running once its step executes, until its step_finished. A CLI run streams nothing to the server: it covers only its targets, as running, until each records a materialization. For a partitioned node, done is the keys with a result (cached, or finished by this run), total every key, and running the keys not done yet while the step runs. When several runs work on one node, the one whose step is running wins, then the oldest. GET /assets/{name} has the same running field. The UI polls this endpoint every 2 s while any node is running (10 s otherwise) and shows the runs on every page, whichever tab or client started them.

The inspection routes (/state, /nodes, /assets, /assets/{name}, /list, /history, /stats/{name}, /runs, /runs/{run_id}, the log routes, /schedule, /plan, /groups, /project and /health) answer every GET with an ETag, a digest of the body, and Cache-Control: no-cache. A request whose If-None-Match names that ETag gets 304 Not Modified and no body while the body is the same. A browser does this by itself, so the UI’s polls of an unchanged project transfer nothing.

Between requests the server keeps what the answers are computed from, each with the evidence that it is still right:

  • the graph it decides cache states with, until a pipeline file, a helper module a step reads or a path an import was looked up at changes (or --watch sees an edit). A graph with keys from partitions(<expression>) is loaded again at least every 30 seconds, since the expression can read anything;
  • nothing of the metadata DB itself: the routes read it in place, through read-only connections beside the runs that write it (SQLite’s WAL mode), so a read never waits for a run and never holds one up, including under --read-only. A database this release would migrate first is read through a private copy, taken on the blocking pool again when the database changes;
  • each barca status result (/state, /nodes, the running fields), until the graph or the database changes. An answer kept for more than 30 seconds is computed again in the background, which bounds how long an artifact deleted by hand, outside barca, still shows as cached.

A finished run, a run on this project by the CLI, a ledger post or an edit is in the next answer. When deciding the cache states takes longer than a second (a graph with a million partition keys takes several), a request after a change is answered with the last result after that second while the new one is computed, and gets the new one once it is ready; the running fields are always current. During a run the database changes many times a second, so a poll waits at most that second instead of a whole computation. If computing the new result fails, the next request computes it while it waits and answers the error. A file is trusted to be unchanged only once it has been still for a moment (2 seconds for source files, a tenth of a second for the database), so a request right after a write computes its answer afresh. While a client polls /state (the UI does every 10 seconds while open), the server computes it again in the background once a change has settled and no run of the server is going, so the poll finds it ready.

POST /state/reset → { database, moved_to, moved, undo }

What the web UI’s Reset database button calls (on the “Can’t load pipeline” panel, and in the sidebar), behind a dialog that says what happens and how to undo it. It works as barca state reset does: under the database’s lock, the metadata database and its -wal and -shm move to metadata.db.reset-<UTC> (and <that>-wal, <that>-shm) in the same directory, and an empty database takes its place, so the next request sees an empty history. Nothing is deleted; artifacts are left alone. A database that cannot be opened or migrated (the case that makes /state fail with Database error: ...) moves as it is. The server keeps no connection open between requests, so the next one opens the new file; the paused schedules and the finished runs it remembered are cleared with the database.

{
"database": ".barca/metadata.db",
"moved_to": ".barca/metadata.db.reset-2026-10-20T18-02-11.482Z",
"moved": [".barca/metadata.db.reset-2026-10-20T18-02-11.482Z", ".barca/metadata.db.reset-2026-10-20T18-02-11.482Z-wal"],
"undo": "barca state restore --from .barca/metadata.db.reset-2026-10-20T18-02-11.482Z"
}

moved_to and undo are null when there was no database. Undo with undo, run with the server stopped. 409 while a run is pending or running (this server’s, or a local run’s on this machine), naming it, with nothing changed; 403 under --read-only; 503 while the server drains. There is no authentication (barca serve has none by design): anyone who can open the UI can reset.

GET /runs/{run_id}/logs?step=report&stream=stderr
→ { "run_id": "...", "status": "running", "logs": [{ node_id, seq, line, stream, ts }, ...] }
GET /logs/{run_id} → { "logs": [{ node_id, seq, line, stream, ts }, ...] }

The lines a run’s steps wrote: stream is stdout or stderr (Python logging, warnings, sys.stderr, and the traceback of each failed attempt), ts when the line was written (RFC 3339 UTC with milliseconds; null on lines recorded before 0.29.2). GET /runs/{run_id}/logs is what barca logs reads: a run in progress answers with its lines so far, an unknown run is 404, and a step that matches nothing (or two definitions) or a stream that is neither is 400. The older GET /logs/{run_id} answers from the database once the run finished; an unknown id is an empty list. A run in progress streams its lines as log events, each with its stream and ts.

Every run the server accepts (POST /runs, the older trigger routes, the UI, a --server client, a scheduled tick) is recorded in the metadata DB (run_queue) under its run id before the server answers. Runs start in the order they were accepted, at most --max-runs at once ([server] max_runs, default one per core), while the queue is neither paused nor draining; the others are pending. A run leaves the queue once it has recorded how it ended; DELETE /runs/{run_id} takes a queued run off it and records it cancelled.

POST /queue/pause holds every queued run back; runs in flight finish, new runs and scheduled ticks still join the queue (one tick per node waits; the next are skipped as in_flight). The pause is kept in server_state, so a restart keeps it. POST /queue/resume lets them start again. barca queue [pause|resume] --server calls these routes.

When the server starts it resumes the queued runs, in order and under their run ids, before its scheduler’s catch-up ([barca] resuming N queued runs). A run the previous server had started and never finished recording (it was killed) is not started again: it is reported on startup and recorded as interrupted.

The first SIGTERM or SIGINT starts a drain. Until the server exits:

  • No queued run starts. POST /runs and the older POST /run, POST /run/{target} and POST /get/{target} still accept runs (202, status: "pending"): they wait in the queue for the next server. GET /queue says draining: true.
  • POST /state/reset and POST /ledger/runs return 503 with the error envelope (kind: infra) and a Retry-After header: the drain timeout in seconds, at least 1.
  • GET /health returns 200 with "status": "draining".
  • The scheduler stops; a tick that falls in the drain starts no run, and the next server’s startup catch-up fires the job once.
  • Reads, GET /runs/{run_id}, /runs/{run_id}/events, DELETE /runs/{run_id} and their older aliases keep working, and the runs in flight finish, artifact uploads included.

The drain lasts until every run in flight has finished (exit 0), for at most --drain-timeout seconds ([server] drain_timeout in barca.toml, default 30). When the deadline passes first, the runs left are stopped and put back at the head of the queue: the next server starts them again from the beginning under the same run ids (at least once: a step that was interrupted runs again), and the server exits 0. A second SIGTERM or SIGINT cancels them instead, as by DELETE /runs/{run_id}, with "error": "run cancelled: server shutdown", the line [barca] run cancelled: server shutdown in their logs and barca.reason: server shutdown on their barca.run span; the server exits 1. The queued runs are kept either way. Open /events streams end once the runs are over, without run_finished for a run still queued; the CLI’s clients reconnect to the next server. See Deploying for supervisor grace periods. The first signal marks the runs in flight draining in the queue: if the server is killed before the drain ends (a grace period that ran out), the next server puts them back in the queue, once, instead of recording them interrupted.

A run that takes longer than its run timeout (--run-timeout, [server] run_timeout_seconds, or Schedule(..., timeout_seconds=N) for a tick; none by default) is stopped and recorded with status error and "error": "run timed out after Ns (<setting>)\n...".

With --read-only, every route that runs, cancels, pauses or resets (POST /runs other than a dry run, DELETE /runs/{run_id}, the schedule pause and resume, POST /state/reset, and the older trigger routes) returns 403 and the scheduler does not start. A current WAL metadata database is read in place through read-only connections, without writing rows, migrating or checkpointing it. SQLite can create -wal and -shm beside it. A database requiring migration (or not in WAL mode) is copied on the blocking pool and migrated privately; a missing DB stays missing. Status warms once at startup even with an active external writer, then after settled changes while a client polls.

GET /assets → [ListNode, ...]
GET /assets/{name} → { "asset": ListNode, "stats": Stats, "running": NodeRunning | null }

ListNode is a barca list --json node: { id, kind, freshness, inputs, env }, with freshness a lowercase word (always, manual, schedule) and, for a scheduled node, the cron expression in schedule and the next fire time in next_fire (local time). {name} matches by function name or full node id; an unknown name returns 404. Stats is the barca stats --json body: { id, total_runs, cache_hit_rate, avg_elapsed_seconds, median_elapsed_seconds, p95_elapsed_seconds, max_elapsed_seconds, max_rss_bytes, median_max_rss_bytes, recent_runs }, each recent run with its run_id, cpu_seconds and max_rss_bytes; the timings are null until the node has run.

GET /plan → { total_steps, pool_size, pool_source, capacity, capacity_source, weights, phases: [{ reason: {type, node_id?}, streams: [{ stream_id, steps }] }], warnings: [{ kind, node, param, message }] }

warnings is always present: the plan-time warnings for every step of the project, [] when there are none (the same items as result.warnings above).

barca serve runs a cron scheduler for nodes declared with freshness=Schedule("..."). It is on by default; pass --no-schedule to turn it off.

Each second, every job whose cron matches triggers a run through the same run pool as the POST endpoints. The run’s targets are the scheduled node and every Always node below it; the walk down goes past a Manual node, which is not updated, and stops at a node with its own Schedule. Each target is brought up to date, not forced:

  • The scheduled node. A sensor is polled; an asset and its upstream cone are checked against the cache; a task runs, reusing cached upstream assets as barca run <task> does (the older POST /run/{task} alias recomputes every upstream asset).
  • Always assets below are computed when they have no cached result for their current run hash.
  • Always tasks below run when their inputs changed since their last successful run (code, an asset input’s result, a sensor input’s value), when a task they read ran successfully since, or when they never ran.
  • Manual nodes are read as they are, never run by a tick. When one has never been computed, the nodes that read it are reported as skipped steps with reason blocked (and as blocked targets), and the tick is not an error on that account.

A tick with Always nodes below its scheduled node is a multi-target run: its outcome in GET /runs/{run_id} has a targets entry per target and steps for all of them. A failing node stops only what depends on it; the other targets still run, and the run’s status is then failed, its error envelope naming the first node that failed. POST /runs reads a Manual node as it is, as barca get and barca run do; the older POST /get/{node}, POST /run/{task} and POST /run compute Manual upstreams as before.

Each scheduled run gets a run_id, is visible via GET /runs/{run_id}, and is written to .barca/metadata.db (barca history), like a run requested over HTTP. Independent runs execute in parallel, bounded by a run pool sized to the machine’s CPUs. Timezone, catch-up after downtime, skipped overlapping ticks and reloading are described in Scheduling.

GET /schedule → [ScheduleEntry, ...]

Each ScheduleEntry is:

{
"id": "pipeline.py:daily_report",
"cron": "0 5 * * *",
"kind": "asset",
"next_fire": 1780740000,
"last_fired": 1780653600,
"last_run": "1b942bb33182",
"last_status": "complete",
"last_skipped": 1780657200,
"skipped_for": "7c0d5e2a91f4",
"skip_reason": "in_flight",
"paused": false,
"paused_until": null,
"failures": 0,
"max_failures": 5,
"failing": false
}

next_fire and last_fired are unix epoch seconds. next_fire is the next match of the cron expression in the zone the server evaluates cron in (--timezone), so it is when the job will fire; next_run in GET /state is computed the same way. barca list cannot know a server’s zone and always uses the local time of the machine it runs on. A job the scheduler has not seen before gets last_fired set to the time the server first started with it, so it is not null even though nothing has run. last_run is the most recent scheduled run_id and last_status its state (pending/running/complete/failed/cancelled, or null if none yet).

A tick is skipped while a run of the same node is pending or running, whether the scheduler, an HTTP request or a barca get / barca run on the same project (.barca/, its process alive) started it (a run of every asset and sensor counts for scheduled assets and sensors, not tasks), and while the job is paused. last_skipped is the time of the last skipped tick (unix epoch seconds), skip_reason why (in_flight or paused) and skipped_for the run_id of the run that was in flight (null for a pause); they are null until a tick is skipped, and a restart clears them.

failures (integer) is the number of consecutive failed ticks of the node, max_failures (integer) its cap from Schedule(cron, max_failures=N) (default 5), and failing (boolean) whether the count has reached the cap. While failing is true the node’s ticks are skipped, and the server logs [barca] pipeline.py:inbox failing: 3 consecutive failed ticks (max_failures=3); skipped until an explicit request or a change in its inputs. The count is kept in the metadata DB and survives a restart. It resets on a successful tick, and when the scheduled node, a node that failed, or a direct input of a failed node succeeds in another run (an explicit request or a change in its inputs). A tick skipped while the job is paused or while a run of its node is in flight is not a failed tick and is not counted. A failing job that is also paused reports skip_reason: "paused".

Each scheduled run is recorded with triggered_by: "schedule", the tick in scheduled_at and the job’s id in schedule_id (GET /runs/{run_id}, barca history --json).

POST /schedule/{id}/pause → the job's ScheduleEntry
POST /schedule/{id}/pause?until=2026-10-21T06:00:00Z
POST /schedule/{id}/resume → the job's ScheduleEntry

{id} is the scheduled node’s full id (percent-encode a / in it) or its name. A paused job’s due ticks are skipped with skip_reason: "paused" (a line on stderr, and a barca.run span with status skipped and reason paused when telemetry is on) until it is resumed or until passes; paused and paused_until (unix epoch seconds, null for a pause until resumed) report it. The ticks skipped while paused are not caught up. Resuming a job that is not paused changes nothing.

A pause is kept in the metadata DB (schedule_state), so a restart, such as a deploy, does not resume it. until must be an RFC 3339 time in the future (400 otherwise); an id that is not a scheduled node is 404, a name that matches several 409, and a read-only server answers 403. When the scheduler has not published the job (--no-schedule, or still starting) the answer is {id, paused, paused_until}.

barca.get(..., server=True) and barca.run(..., server=True) (or server="<url>") run a command on the server through the CLI’s --server and return the value, as a local call does. A parquet or pickle result is read from GET /runs/{run_id}/output.

barca.Client is deprecated in favour of them: constructing one emits a DeprecationWarning, and it will be removed in a later minor release. The standard-library SDK wraps this API:

from barca import Client
c = Client("http://127.0.0.1:8274")
run = c.get("daily_report") # POST /get/{target} (alias), returns immediately
result = run.wait(timeout=30) # poll /status (alias) until complete/failed
print(result["status"])
for job in c.schedules(): # GET /schedule
print(job["id"], job["cron"], job["next_fire"])

Client methods map to the endpoints above: health(), assets(), asset(name), plan(), schedules(), status(run_id), cancel(run_id) (also available as Run.cancel()), plus the two trigger verbs that mirror the CLI — get(target=None) (omit the target to get every asset and sensor, never tasks) and run(target). The trigger methods return a Run with .status(), .cancel() and .wait(timeout=600.0, poll=0.5), which blocks until the run is complete, failed or cancelled. Client() defaults to http://127.0.0.1:8274. There are no client methods for /state, /events or /logs. barca.get, barca.run and the other functions in barca.api are separate: they start the barca binary for one command and do not talk to a server.

Client still talks to the older routes, retained as deprecated compatibility aliases; its statuses are those of GET /status/{run_id} (complete, not success).

Every error body is the CLI’s stderr error envelope (CLI contract):

{ "error": "Asset 'nope' not found. Available: pipeline.py:orders, pipeline.py:total",
"code": 2, "kind": "usage",
"remediation": "Run `barca list` to see available assets and tasks." }

kind and code are those a local command exits with (usage 2, infra 3). reason is present only on an error a client may handle on its own. metadata_unusable (from 0.28) means the metadata DB is there but this barca cannot use it; error names the file and remediation says what to do, instead of a SQL error such as Parse error: no such column:

{ "error": "the metadata DB .barca/envs/prod/metadata.db cannot be used: metadata schema version 2 is not supported by barca 0.28.0 (supports version 1 and legacy version 0); use a compatible Barca release. The database was not migrated or reset",
"code": 3, "kind": "infra", "reason": "metadata_unusable",
"remediation": "Not a problem in your code. If a newer barca wrote this database, upgrade barca. Otherwise put a backup back with `barca state restore` (it moves .barca/envs/prod/metadata.db aside first), or start an empty history with `barca state reset` (the Reset database button in the web UI of a running `barca serve`), which moves .barca/envs/prod/metadata.db aside and prints how to undo it." }

The UI shows the remediation under “Can’t load pipeline”, above its Reset database button. The HTTP status follows the kind: usage is 400 (parse and DAG errors, a target of the wrong kind, a bad request body), 404 for something that does not exist (an unknown node, run or route), 409 for a name that matches several nodes or a request that conflicts with a run’s state (cancelling a finished run, the output of a run still going), and 403 for a run, cancel or pause asked of a --read-only server; infra is 500, and 503, with a Retry-After header, for a reset or a ledger post asked of a server that is draining before it stops. A 405, for a known path asked with the wrong method, names the allowed methods in error and in the Allow header. A failed step is never an HTTP error: it is a run whose outcome is failed. The Python client raises BarcaError carrying the status and the error message for each of these.

  • No authentication and no TLS. The server listens on 127.0.0.1 by default; --host can make it reachable from other machines. Authenticate at a proxy.
  • One server per project. Machines share results by pointing at it (the ledger); a second server on the same project, or a bucket on its own, shares nothing (RFC-0007).
  • Runs in progress are not resumed after a restart. A finished run’s outcome is kept and GET /runs/{run_id} answers it; a run still queued at the restart never started.
  • One machine: steps run in worker processes on the host that runs the server.

GET /assets/{name}/schema reads a private metadata snapshot and inspects the selected node and its direct inputs using the same reader as barca status. It returns NodeStatus[]. Each node’s shape contains type, rows and columns: [{name, type}] when the artifact supports them. JSON objects report field names and types, JSON lists also report item_types; pickle reports its top-level type without unpickling. Reader failures appear in shape.note. No artifact or a failed latest attempt gives shape: null. For partitioned nodes this describes the key barca status reports (the latest run’s first key in sort order), named in last_materialization.partition, rather than a union of every key’s schema. No sample values are returned. Nested JSON fields report their container type (dict or list); this is not a recursive schema. Pickle types or constructors are identified from opcodes without executing them, and pickle column schemas are unavailable. This endpoint is available in read-only mode; unknown and ambiguous names return 404 and 409 respectively.