Skip to content

Run and inspect

Start one full build, follow its progress, and verify what it delivered.

Terminal window
mr-data run --recipe RECIPE_ID --digest RECIPE_DIGEST --full --json

Both 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.

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.

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.

{
"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 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.

Terminal window
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.

Terminal window
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.

Terminal window
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.

Four reads. None waits on anything and none mutates a run.

Terminal window
mr-data peek RUN_ID --json # columns, logical types, first rows
mr-data query RUN_ID "select count(*) from run_table" # one bounded read-only question
mr-data checks RUN_ID --json # every check the recipe declared
mr-data receipt RUN_ID --json # sources, digests, coverage, snapshot members

Any 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.

Terminal window
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.

Terminal window
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.000000Z

Studio 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.

Terminal window
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.

Terminal window
mr-data receipt RUN_ID --json

The 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.

Terminal window
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.

Terminal window
mr-data verify RUN_ID --deep --json

Checks 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.

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 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.

Terminal window
mr-data parts TABLE_ID --json
mr-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.

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.

Terminal window
mr-data run --recipe RECIPE_ID --digest RECIPE_DIGEST --full --json

Start 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, running or succeeded: 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, read sample_run_id and inspect the preview before an authorized editor releases the full run. Repair holds require a person in a signed-in browser. --confirm settles 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.
Terminal window
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.