Run and inspect
Start one full build, follow its progress, and verify what it delivered.
Start a full build
Section titled “Start a full build”mr-data run --recipe RECIPE_ID --digest RECIPE_DIGEST --full --jsonBoth identifiers are required and neither is a positional, so mr-data run RECIPE_ID is refused. --recipe names the registered recipe. --digest states which bytes of it to run, as the bare lowercase hex recipe_digest the registration printed, with no sha256: prefix. The client never derives it, and a digest that disagrees is RECIPE_DIGEST_MISMATCH.
Full builds use progressive acquisition by default. An inspection checkpoint becomes available after about five minutes, and the same run continues automatically. The checkpoint does not pause for approval, start another full run, or fetch captured pages again. Five minutes is the checkpoint target, not a runtime limit. A spend confirmation, when required, happens before acquisition starts.
Use --sample with an explicit ceiling only for a deliberately bounded experiment. Its row ceiling limits output, not acquisition time: a collection may fetch all its pages before truncating rows. A standalone sample is not the progressive checkpoint. Use source inspection for bounded research before building.
Every flag
Section titled “Every flag”| Flag | What it does |
|---|---|
--recipe RECIPE |
The registered recipe to run, by its identifier. |
--digest DIGEST |
The recipe digest those bytes carry. Bare lowercase hex, checked and never derived. |
--mode MODE |
sample, full, refresh, backfill, compact or replay. Accepted for all six. |
--sample |
Shorthand for --mode sample. Run an explicit bounded experiment. State at least one ceiling; row limits do not limit acquisition time. |
--full |
Shorthand for --mode full. Build the whole table; full builds use progressive acquisition automatically. Over the projection threshold it is held for confirmation before acquisition. |
--refresh |
Shorthand for --mode refresh. Run forward from where the last successful run reached. |
--backfill |
Shorthand for --mode backfill. Run one exact window, stated with --window. |
--sources-from RUN_ID |
With --mode replay only, replay the retained raw inputs of that successful run. Replay never becomes live. On any other mode it is refused THIN_ARGUMENT_INVALID before anything is sent. |
--max-rows MAX_ROWS |
Stop at this many rows from each source. |
--max-source-bytes MAX_SOURCE_BYTES |
Stop before fetching more than this from the sources. Exceeding it fails every mode because bytes already transferred cannot be truncated. |
--window START END |
The half-open time range to cover. The start is included and the end is not. The client counts it as a sample ceiling. The current worker rejects a window clamp on sample or full with RUN_CLAMPS_INVALID, so clamp a sample with --max-rows or --max-source-bytes. |
--resource-class RESOURCE_CLASS |
The capacity class to ask for. Studio rejects one it cannot satisfy. |
--confirm |
Authorize the run if it is held for a confirmation, instead of printing the projection. |
--cancel RUN_ID |
Cancel this run instead of starting one. Legal while it is queued, held or running. |
--json |
One JSON object instead of human lines. |
--no-activity |
Do not tell the dataset page what this command is doing. |
--max-rows is per source, not a table size
Section titled “--max-rows is per source, not a table size”A recipe with eight sources delivers up to eight times the number asked for. 2,000 becomes 16,000. Read the delivered size off the run’s coverage and report that number, not the one passed.
Eight sources each cut at 2,000 rows are eight partial spans stitched into one table. The seams exist nowhere in the data, and a continuity or completeness check that passes over the whole table fails over the sample. Measure the sources, then set the ceiling above the largest one. coverage.truncated then comes back false on every entry.
The three clamps do not behave alike
Section titled “The three clamps do not behave alike”On sample, max_rows and window truncate. The run succeeds at the clamp boundary and reports the coverage it delivered. Exceeding max_source_bytes fails every mode, including sample. On full, refresh and backfill, hitting any clamp returns CLAMP_EXCEEDED.
A deployed worker may accept a narrower set of modes and clamps. A mode or clamp outside that set comes back as RUN_MODE_INVALID or RUN_CLAMPS_INVALID. Report it rather than working around it.
What run answers with
Section titled “What run answers with”{ "schema_version": "mostlyright-thin-client-v4-run.v1", "status": "run_queued", "lane": "hosted", "dashboard_url": "https://mostlyright.md/…", "watch_command": "mr-data watch RUN_ID", "run": { "run_id": "…", "workspace_id": "…", "recipe_id": "…", "recipe_digest": "…", "dataset_id": "…", "table_id": "…", "mode": "full", "status": "queued", "clamps": {}, "resource_class": "…", "created_at": "2026-09-12T10:00:00Z", "completed_at": null, "coverage": null, "table_version_id": null, "candidate_digest": null }}A run held at the confirmation gate answers status: "run_awaiting_confirmation" with projected_bytes, projected_runtime_s, projected_cost and a confirm_command restating every argument the request carried. It exits 2. The same command again with --confirm settles it against the number already on the record. Any authenticated caller of the paying workspace may settle it.
A terminal run is cached
Section titled “A terminal run is cached”A finished run is cached against (recipe, digest, mode, clamps). Re-running that exact cache key replays the stored result, a stored failure included, and consumes no worker. To force a new attempt, change a clamp. --full --max-rows 50000 is a different cache key from a bare --full.
Follow the run
Section titled “Follow the run”mr-data watch RUN_ID --json| Flag | What it does |
|---|---|
run_id |
The run to follow, by the identifier the start printed. |
--from-seq FROM_SEQ |
Replay from after this log position. Ignored once a cursor is known. |
--last-event-id LAST_EVENT_ID |
Resume from this cursor, as a previous watch reported it. |
--json |
One JSON object instead of human lines. |
Each event prints under the durable event type of the stage that produced it. The command returns a summary when the run reaches a terminal state, and exits non-zero when the run failed.
Resuming is the normal case. Studio closes the stream before its request deadline, and watch reconnects from its own cursor with no gap and no duplicate. The receipt reports last_event_id, last_sequence and reconnects. A stream that closes with the reason run_terminal never appends again, and watch stops.
A run in queued has appended nothing yet. Progress events are best effort by contract and are never evidence. A run that emitted none produces the same build as a run that emitted all of them. Never turn a progress count into a percentage, and never cite a progress event where a receipt is asked for.
Read the record
Section titled “Read the record”mr-data status RUN_ID --json| Flag | What it does |
|---|---|
run_id |
The run to report. |
--receipts |
No effect. A run’s digests, sources, timings, checks and causes are all on the record this command already reads. Passing it reports that under flags_without_effect. |
--json |
One JSON object instead of human lines. |
{ "schema_version": "mostlyright-thin-client-v4-run-status.v1", "status": "run_status_reported", "lane": "hosted", "run": { }, "terminal": true}A failed run adds failure, containing failure_code, failure_detail and failed_stage, and a refusal with the same code and a sentence. status exits 2 on a failed run. A run awaiting_confirmation adds projection. A preview-first pair names both halves: sample_run_id and full_run_id.
A run has seven states: queued, running, awaiting_confirmation, awaiting_sample_approval, succeeded, failed, cancelled. Three of them are terminal and end a watch: succeeded, failed, cancelled.
mr-data runs --status succeeded --mode sample --limit 20 --json| Flag | What it does |
|---|---|
--status STATUS |
One of the seven states. The client refuses a word outside them with THIN_ARGUMENT_INVALID. |
--mode MODE |
One of the six modes. Same refusal. |
--limit LIMIT |
How many runs to report. Twenty by default. The client follows the workspace’s own pages until it has that many or there are no more. |
--json |
One JSON object instead of human lines. |
The answer carries count, runs, next_cursor and the filters it applied.
Interrogate the result
Section titled “Interrogate the result”Four reads. None waits on anything and none mutates a run.
mr-data peek RUN_ID --json # columns, logical types, first rowsmr-data query RUN_ID "select count(*) from run_table" # one bounded read-only questionmr-data checks RUN_ID --json # every check the recipe declaredmr-data receipt RUN_ID --json # sources, digests, coverage, snapshot membersAny successful run is readable straight away. No approval, admission or release step sits between a run finishing and reading it.
| Flag | What it does |
|---|---|
target |
The hosted run, by the identifier the submission printed. |
--rows ROWS |
How many of the preview’s rows to show. It narrows what is shown and cannot widen it. The run fixed the preview’s size when it wrote the preview. |
--json |
One JSON object instead of human lines. |
--no-activity |
Do not tell the dataset page what this command is doing. |
The client also accepts --format, --reader, --reader-options, --allow-redirect-host, --json-records-pointer, --json-expand, --json-column, --json-optional-column and --json-document-format. None of them does anything here, because the hosted preview arrives already decoded. The client reports the ones passed under flags_without_effect.
{ "schema_version": "mostlyright-thin-client-v4-peek.v1", "status": "peeked", "lane": "hosted", "run_id": "…", "dashboard_url": "https://mostlyright.md/…", "artifact_id": "…", "row_count": 731, "columns": ["station", "observed_on", "max_temp_c"], "schema": {"column 1": {"name": "station", "type": "string"}}, "sample": {"row 1": {"station": "LKPR", "observed_on": "2024-01-01"}}, "input_sha256": "…"}The preview artifact carries column names alone, so the type comes from the run’s own column_profile, the same artifact diff compares schemas with. A type of null means the run recorded no profile for that column, not that the column is untyped. Check the run reached persist before reporting a schema from it.
mr-data query RUN_ID "select count(*) from run_table" --json| Flag | What it does |
|---|---|
run_id |
The run whose table to ask. |
SQL |
Read-only, one statement, beginning with SELECT, WITH, EXPLAIN or DESCRIBE. |
--max-rows MAX_ROWS |
Stop at this many rows. Left out, the server’s own ceiling applies. Studio refuses a number above that ceiling rather than lowering it. |
--field FIELD |
Which column --from and --to are about. Without either it narrows nothing, so the client refuses it rather than leave a flag that looks like a filter and did nothing. |
--from FROM_BOUND |
Open only the parts whose recorded range on --field reaches this value or later. Inclusive, and never SQL. |
--to TO_BOUND |
Open only the parts whose recorded range starts before this value. Exclusive, so a day range is from one midnight to the next. |
--partition NAME=VALUE |
Open only the parts whose partition NAME is one of the values given, one --partition each. The read opens any part that carries no value for NAME. |
--no-session |
When no warm worker holds the workspace, refuse rather than open a research session for the run’s dataset. |
--json |
One JSON object instead of human lines. |
--no-activity |
Do not tell the dataset page what this command is doing. |
The relation is always named run_table, never the name the recipe gave the table. A statement naming the recipe’s table gets a catalog error suggesting pg_tables, which reads like a permissions problem and is not one.
Studio queues the question, and the command reads the answer back at most 40 times and for at most 120 seconds, whichever comes first. It is never a long poll. A wait that ends first leaves the question running and prints its identifier. Running the same command again reads that one back, because the identifier comes from the statement, the row limit and the prune block.
{ "schema_version": "mostlyright-thin-client-v4-query.v1", "status": "query_answered", "lane": "hosted", "run_id": "…", "query_id": "…", "dashboard_url": "https://mostlyright.md/…", "replayed": false, "polls": 2, "state": "succeeded", "sql": "select count(*) from run_table", "max_rows": null, "prune": { }, "part_count": 2, "row_count": 1, "truncated": false, "elapsed_ms": 412, "result_digest": "…", "rows": {"row 1": {"count_star()": 731}}}A table version is many Parquet parts, and a bounded read may open only so many. Narrow with --field plus --from/--to, or with --partition. That block is never SQL. Studio evaluates it against the part list before waking a worker. It opens any part it cannot prove irrelevant, so pruning never drops rows the question would have matched. The bounds are the ones mr-data parts prints beside each part.
mr-data query RUN_ID "select count(*) from run_table" \ --field observed_at --from 2026-03-01T00:00:00.000000Z --to 2026-03-02T00:00:00.000000ZStudio does not start a worker on a query. When no worker is warm, query opens a session for the run’s own dataset. It waits for the worker, asks, and closes the session behind the answer. --no-session prints the refusal instead, carrying a retry window to wait through.
The statement gate does not read SQL. It blanks comments and quoted text, then scans what is left on word boundaries. A column literally named system, copy, set, load, call, glob, import or export is refused by name. Write the identifier in "double quotes", or project it under another name. The boundary is the worker’s read-only engine. It can open no file, load no extension and reach no network.
| Refusal | What it means |
|---|---|
QUERY_STATEMENT_FORBIDDEN |
The statement gate refused, naming the offending token. |
QUERY_ROW_LIMIT_EXCEEDED |
The question asked for more rows than the workspace’s ceiling allows. |
QUERY_RESULT_TOO_LARGE |
The answer was over the byte ceiling. An error, never a truncation. |
QUERY_TIMEOUT |
The statement outran its deadline. |
QUERY_TABLE_UNAVAILABLE |
The run has no readable table. It did not succeed, or its artifact is not there. |
QUERY_NO_WARM_EXECUTOR |
No warm worker holds this workspace. Only reachable with --no-session. |
QUERY_TOO_MANY_PARTS |
The question would open more parts than a bounded read may open. The refusal names the selected count and the ceiling. |
Two things are broken here. Aggregating a timestamp column fails: min(obs_time) answers Required module 'pytz' failed to import, because the query image lacks it. Aggregate the rendered value instead, min(CAST(obs_time AS VARCHAR)). And mr-data receipt on some succeeded runs answers THIN_STUDIO_SCHEMA_VALIDATION_FAILED. Read mr-data status --json for coverage and mr-data download --kind receipt for the recorded receipt.
checks
Section titled “checks”mr-data checks RUN_ID --json{ "schema_version": "mostlyright-thin-client-v4-checks.v1", "status": "checks_reported", "lane": "hosted", "run_id": "…", "run_status": "succeeded", "checks_scope": "version", "checks": {"check 1": {"check_id": "one_row_per_station_day", "passed": true, "scope": "version"}}, "check_count": 4, "checks_passed": 4, "checks_failed": 0}It exits non-zero when one did not pass. A check the recipe declared as required does not reach this command at all. A required check that fails ends the run with failure_code: CHECK_FAILED, and this command then reports the failure triple in full rather than an empty table.
checks_scope is the relation the pass covered. version means every row of the table the run composed. Anything else means the rows that run read and not the ones it inherited. Never report “the checks passed” over an append without saying which rows they were about.
receipt
Section titled “receipt”mr-data receipt RUN_ID --jsonThe receipt is the record of how the run was built. It names every source the run fetched and what those bytes hashed to. It carries the coverage delivered and the members of the raw snapshot. The answer carries artifact_id, digest, size_bytes and the receipt document itself.
A receipt that could not be read says why. THIN_NOT_FOUND is the ordinary state of a run that has not produced that record yet. THIN_AUTHORIZATION_DENIED means the credential may not see it.
mr-data diff RUN_A RUN_B --json| Flag | What it does |
|---|---|
left, right |
The two runs to compare. |
--columns-only |
Compare the columns alone, out of each run’s column profile. Without it the comparison also takes in the row count, the window covered and the data’s own fingerprint. |
--json |
One JSON object instead of human lines. |
status is builds_identical or builds_differ, beside a summary, a change count, the numbered changes, and the first run and second run that were read.
verify
Section titled “verify”mr-data verify RUN_ID --deep --jsonChecks the run’s table manifest against the parts it names. Without --deep no part is fetched, so a clean answer does not prove every part’s bytes are still what the manifest says. With --deep every part is fetched and re-hashed against the digest the manifest states. It exits non-zero when anything disagrees.
Read the coverage before you say anything
Section titled “Read the coverage before you say anything”A succeeded run carries coverage, and truncated is required on every entry. It separates a complete acquired response from one cut at fifty thousand rows, without opening the receipt.
Read truncated first. False on every source means the worker acquired each source whole. The table this run wrote is the table a full run would have written. True anywhere means the result is a shape rather than an answer.
A null coverage window means the acquisition was unwindowed, not that it covered all time. Never present a sampled row as a whole-population value, and never call a run verified because it succeeded.
A table made of parts
Section titled “A table made of parts”A table version is one manifest document naming immutable Parquet parts of about 128 MiB each. A billion-row table is a few hundred of them. There is no single file to fetch and no flag that makes one.
mr-data parts TABLE_ID --jsonmr-data parts TABLE_ID --version VERSION_ID --field observed_at --from 2026-03-01 --to 2026-04-01| Flag | What it does |
|---|---|
table_id |
The table to read, by its identifier. |
--version VERSION |
Which version to list. The live one when this is not given. |
--from FROM_BOUND |
Keep only the parts whose recorded range reaches this value or later. |
--to TO_BOUND |
Keep only the parts whose recorded range starts before this value. |
--field FIELD |
Which field --from and --to are about. The version’s first sort field by default. |
--json |
One JSON object instead of human lines. |
{ "schema_version": "mostlyright-thin-client-v4-table-parts.v1", "status": "parts_listed", "lane": "hosted", "table_id": "…", "table_version_id": "…", "table_version_number": 3, "manifest_digest": "…", "row_count": 1043221, "part_count": 9, "total_bytes": 1148846080, "partition_fields": ["station"], "sort_fields": ["observed_at"], "parts": [ { "part_id": "…", "part_key": "…", "record_count": 128000, "file_size_in_bytes": 134217728, "lower_bounds": {"observed_at": "…"}, "upper_bounds": {"observed_at": "…"} } ], "parts_listed": 9}Listing them downloads nothing, and reading the listing does not prove any part is still readable. part_id is the sha256 of the part’s bytes, and whoever reads a part checks its bytes against it. mr-data download and mr-data verify RUN --deep both check the parts.
Pin the version for anything reproducible. Record table_version_id and manifest_digest, then pass --version VERSION_ID from then on. --version takes the identifier and never the table_version_number printed beside it.
The full run
Section titled “The full run”Untruncated coverage makes a table finished, not the mode the run was started in. Calling a complete table a sample tells somebody their dataset does not exist yet. Do not rebuild one to change the run mode.
If you deliberately requested a standalone sample and it was truncated, name what was cut and on which sources, and explain what the full build would cover before starting it within the user’s authorization. An ongoing progressive full build already continues beyond its checkpoint; keep following that run.
mr-data run --recipe RECIPE_ID --digest RECIPE_DIGEST --full --jsonStart it bare, with no ceiling and no resource class. One cache key is one run, and a clamp would make a second.
The same decision is a button on the dataset page, so the answer can arrive as a run you did not start. Read mr-data runs --json --mode full before acting on this recipe again. For any run newer than the sample that you did not start, read mr-data status RUN_ID --json for the recipe and digest it names. One naming this pair, with empty clamps and the default resource_class, is the page’s run.
queued,runningorsucceeded: adopt it as the full run you would have started.awaiting_confirmation: the confirmation gate, settled from the page or by running the command again with--confirm.awaiting_sample_approval: an existing legacy preview hold, or a repair hold. New full builds do not create sample/full pairs. For a legacy preview hold, readsample_run_idand inspect the preview before an authorized editor releases the full run. Repair holds require a person in a signed-in browser.--confirmsettles the confirmation gate, not this one.failed: read its triple and report it. Do not start another. A bare run of this cache key replays that failure, and a new attempt is a revision.cancelled: the decision was withdrawn.
Bring the bytes back
Section titled “Bring the bytes back”mr-data download RUN_ID --output DIR --json| Flag | What it does |
|---|---|
run_id |
The run to read. |
--output OUTPUT |
The folder to write into. run-artifacts by default, and nothing in it is ever overwritten. |
--kind KIND |
Which artifacts to bring back, one --kind each: table_parquet, table_manifest, table_part, column_profile, preview, receipt, raw_snapshot. |
--json |
One JSON object instead of human lines. |
--no-activity |
Do not tell the dataset page what this command is doing. |
Without --kind, you get the shape the run wrote. A version made of parts brings table_manifest, every table_part and the receipt, written as table/manifest.json beside table/parts/<part_key>.parquet. A table written whole brings table_parquet and the receipt.
The client checks each artifact’s bytes against the digest Studio recorded, then writes the file. A table version made of parts arrives whole. The manifest names every part of the version, including the ones earlier runs wrote, and each comes back through the version’s own signed reads. If a part cannot be fetched, the refusal names how many failed. The command never leaves a folder holding only some of the table.
A finished table is downloadable whatever mode produced it.
Publish and keep it fresh covers what happens after the first succeeded run. Fix a failed run covers the refusals.