# Run and inspect

Source: https://mostlyright.md/docs/build/run-and-inspect/

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

## Test before the full build

A new table's first successful run goes live, including a sample. Never use a real table's first sample as a preflight for its full build.

For a public CSV that needs no credential, use [source inspection](/docs/build/probe-sources/). To inspect a complete bounded run, copy the recipe with a different `table.name`, remove `table.search` from the copy, and use small source budgets. A collection needs a small `pages.max_fetches_per_run`; a per-row request collection also needs `pages.stop_at_budget: true`. `--max-rows` alone does not shorten a page walk.

Register that scratch recipe and use **its returned identifiers**:

```sh
mr-data run --recipe SCRATCH_RECIPE_ID --digest SCRATCH_RECIPE_DIGEST --sample --max-rows 100 --json
mr-data watch SCRATCH_RUN_ID --json
mr-data peek SCRATCH_RUN_ID --json
mr-data checks SCRATCH_RUN_ID --json
```

After inspection, retire only the scratch table:

```sh
mr-data demote SCRATCH_TABLE_ID --json
mr-data table archive SCRATCH_TABLE_ID --confirm DELETE --json
```

For a bounded recipe test report instead, use `mr-data recipe test RECIPE_ID --json`. This is a separate test operation, not a `run --sample` on the live table. Read its skipped-source and coverage information; a small test does not prove full coverage or that every full-table check will pass. See [recipe test](/docs/reference/cli/recipe/#recipe-test).

Provider-paid requests can spend money even in a test. Forecast them and use the user's existing authorization before making those calls. Scratch captures are not reused by the real table.

## Start a full build

```bash
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 size confirmation, when required, happens before acquisition starts. It is a size check, not a Mostly Right build charge; upstream providers may charge separately.

Use `--sample` with an explicit ceiling only on a separate scratch table 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

| 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` | Accepted, and has no effect from 0.27.1: this command tells the dataset page nothing. |

### `--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, unless a sample of a source with a request window starts after that source's `start_at` (below).

### 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 sample can read less of a source with a request window, and no clamp is involved. It asks the publisher for that source's latest `lookback_seconds` only, rounded up to whole days, at least one day, and never starting before the source's `start_at`. A sample that starts after `start_at` records `truncated: true` on that source. A check on the whole table, such as a row floor, can then fail on the sample although the recipe and the source are sound. A source that also declares `refresh_readiness` is read only up to its last completed day. When such a source has no completed day left to read, a sample without `--window` does not run at all: it is refused `WINDOW_NOT_READY`, or it waits in the queue until Studio fails it. A full run is what verifies a check on the whole table, once the full build is authorized. From 0.27.1 registration names these sources under `warnings`, the first twelve by name and the rest as a count.

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

```json
{
  "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

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

```bash
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

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

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

```bash
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

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

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

### `peek`

| 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` | Accepted, and has no effect from 0.27.1: this command tells the dataset page nothing. |

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

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

### `query`

```bash
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` | Accepted, and has no effect from 0.27.1: this command tells the dataset page nothing. |

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.

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

```bash
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 too large or nested too deep. An error, never a truncation. Ask for less, or cast a deeply nested column to text. |
| `QUERY_RESULT_INEXACT` | The answer holds a value that cannot be rendered exactly, such as a `FLOAT`, an `INTERVAL` or a zoned timestamp inside a union. The refusal names the column. Cast that column to text in the statement. |
| `QUERY_RESULT_REFUSED` | Studio refused to record the answer, so the query failed at once instead of waiting out its deadline. The refusal says why and what to do. |
| `QUERY_TIMEOUT` | The statement outran its deadline. |
| `QUERY_DOWNLOAD_TIMEOUT` | The deadline ran out while the worker was still downloading the parts the question selected, so the statement never ran. The download and the statement share one deadline. The refusal names the deadline, how many parts had arrived, and how long the query waited to be picked up when that was half a second or more. When the parts could not fit even the whole deadline at the rate they arrived, narrow the question with `--field` plus `--from`/`--to`, or `--partition`. Otherwise ask again. A `WHERE` does not shrink the download. |
| `V4_QUERY_TABLE_UNREACHABLE` | The worker could not download the table. Storage answered with an error, sent nothing, or the connection failed before or partway through the download. Ask again, unless the refusal says the run's table is larger than a bounded query may open. |
| `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. |

A column that carries a timestamp with a time zone comes back as UTC instants with a `+00:00` offset, such as `2026-09-17T20:52:00+00:00`. Both `select obs_time` and `min(obs_time)` answer that way, and so does a timestamp nested in a list, a struct or a map. The statement needs no cast to text.

Floats, intervals and very large integers come back as text, and need no cast either:

| Value | Answer |
| --- | --- |
| `DOUBLE` | Its shortest round-trip text, such as `"1.5"`. An `avg` answers text such as `"1683328161.1104028"`. NaN, the infinities and negative zero come back as `"NaN"`, `"Infinity"`, `"-Infinity"` and `"-0.0"`. |
| `FLOAT` | The engine's own text, such as `"0.1"`. |
| `INTERVAL` | The engine's own text, with its months kept, such as `"1 month"`. |
| An integer past ±(2^53 − 1) | Its digits as a string, such as `"9007199254740993"`. Smaller integers stay JSON numbers, so one column can mix the two. |

One thing is broken here. `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`

```bash
mr-data checks RUN_ID --json
```

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

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

### `diff`

```bash
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`

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

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

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.

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

```json
{
  "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

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.

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

## Bring the bytes back

```bash
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` | Accepted, and has no effect from 0.27.1: this command tells the dataset page nothing. |

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.

## Next

[Publish and keep it fresh](/docs/build/publish-and-refresh/) covers what happens after the first succeeded run. [Fix a failed run](/docs/build/troubleshooting/) covers the refusals.
