# Run states and errors

Source: https://mostlyright.md/docs/reference/run-states-and-errors/

A run moves through seven states, and a failed one carries a code, a detail and the stage it failed at.

## Run states

`run.status` is the state machine `contracts/state-machines/run-v4.json` declares.

| State | Terminal | Meaning |
| --- | --- | --- |
| `queued` | no | accepted and waiting for a worker |
| `running` | no | a worker holds it |
| `awaiting_confirmation` | no | the run is over the projection threshold and needs a `--confirm` before it starts |
| `awaiting_sample_approval` | no | an existing legacy preview hold or a repair hold; new full builds do not create sample/full pairs |
| `succeeded` | yes | it finished |
| `failed` | yes | it stopped, and the record says why |
| `cancelled` | yes | somebody cancelled it, or a gate expired |

The transitions, and who may make them:

| Transition | From | To | Actor |
| --- | --- | --- | --- |
| `create` | none | `queued` | workspace editor |
| `require_confirmation` | none | `awaiting_confirmation` | workspace editor |
| `require_sample_approval` | none | `awaiting_sample_approval` | workspace editor |
| `confirm` | `awaiting_confirmation` | `queued` | workspace editor |
| `approve_full` | `awaiting_sample_approval` | `queued` | workspace editor |
| `expire_confirmation` | `awaiting_confirmation` | `cancelled` | Studio |
| `expire_sample_approval` | `awaiting_sample_approval` | `cancelled` | Studio |
| `abort_sampled_full` | `awaiting_sample_approval` | `cancelled` | Studio |
| `start` | `queued` | `running` | Studio |
| `succeed` | `running` | `succeeded` | Studio |
| `fail` | `queued`, `running` | `failed` | Studio |
| `terminalize_lost_execution` | `running` | `failed` | Studio |
| `cancel` | `queued`, `awaiting_confirmation`, `awaiting_sample_approval`, `running` | `cancelled` | workspace editor |

A failed run carries `failure_code`, `failure_detail` and `failed_stage`, or it is not a valid record.

`mr-data status RUN_ID` exits 2 on a `failed` run. `mr-data run` exits 2 when the run comes back `awaiting_confirmation`.

## Outcome

A succeeded run carries `outcome`:

| Value | Meaning |
| --- | --- |
| `version` | the run wrote a new table version. An absent member means the same |
| `unchanged` | every source's bytes matched the predecessor's, so the worker stopped after acquisition. No transform, no write, no upload, no receipt, no table version. The table's `last_refresh` advances and the live pointer stays put |

`unchanged` is legal only on mode `refresh`. An initial, full, sample or backfill has no predecessor to compare against.

## Run modes

One run command takes six modes.

| Mode | Flag | What it does |
| --- | --- | --- |
| `sample` | `--sample` | an explicit bounded experiment. State at least one ceiling; row limits do not limit acquisition time |
| `full` | `--full` | one progressive acquisition of the whole build. Any required confirmation happens before acquisition; its inspection checkpoint continues automatically |
| `refresh` | `--refresh` | forward from where the last successful run reached |
| `backfill` | `--backfill` | one exact window, stated with `--window START END` |
| `compact` | `--mode compact` | rewrites adjacent parts of an existing version. Reads no source |
| `replay` | `--mode replay` | restores the retained raw bytes of `--sources-from RUN_ID`. Never advances live state and never acquires from a publisher |

## Worker stages

`failed_stage` is one of five, in order.

| Stage | What runs |
| --- | --- |
| `acquire` | the worker fetches or redeems every declared source |
| `parse` | the Reader decodes the bytes into a relation |
| `transform` | the declared SQL steps run over the source relations |
| `check` | the worker evaluates the declared checks |
| `persist` | the worker uploads and stores the table version and its artifacts |

The acquire stage reports three phases: `fetching`, `fetched`, `refused`.

## Run shape

Studio stamps `run.shape` at admission and never re-derives it.

| Shape | What it is |
| --- | --- |
| `warm` | the warm session-worker pool |
| `ready` | the general ready worker pool, capacity-based |
| `ingestion` | the bounded-direct ingestion path, legal only for a `source-history.v2` bounded direct refresh |
| `batch` | a batch job |

A source with a Reader pin, a generic API configuration, or a document projection needs a per-child memory cgroup and a capture-store mount. Admission stamps it `batch`. Both pool shapes fall back durably to batch when nothing claims them.

## Run events

`mr-data watch RUN_ID` reads an append-only log. `event_type` has eight values, each with a writer.

| Event | Written by |
| --- | --- |
| `run_progress_acquire` | the worker, once per acquire progress tick |
| `run_progress_parse` | the worker |
| `run_progress_transform` | the worker |
| `run_progress_check` | the worker |
| `run_progress_persist` | the worker |
| `run_succeeded` | the worker's terminal callback |
| `run_failed` | the worker's terminal callback, or Studio's lease watchdog |
| `run_narrative_appended` | `POST /v4/runs/{run_id}/narrative`, one per accepted cell |

No `run_created`, `run_started` or `run_cancelled` event exists. Nothing writes them. Each record carries `schema_version`, `event_id`, `workspace_id`, `run_id`, `cursor` (the SSE `id`), `sequence`, `event_type`, `occurred_at`, `actor_principal_id` and `payload_digest`. Every one of those fields is mandatory. `attempt_id` and `unsealed` are optional, and the record accepts nothing else. A progress message is at most 1024 characters. A progress identifier is at most 63.

## The refusal envelope

Studio v4 returns an error as `application/problem+json` with an `x-correlation-id` header.

```json title="a v4 refusal"
{
  "schema_version": "4.0.0",
  "code": "RECIPE_DIGEST_MISMATCH",
  "message": "The registered recipe does not have that digest.",
  "request_id": "a2f4…",
  "details": [
    { "pointer": "/document/sources/0/limits", "reason": "max_source_bytes is required" }
  ]
}
```

| Field | Required | Shape |
| --- | --- | --- |
| `schema_version` | yes | `4.0.0` |
| `code` | yes | `^[A-Z][A-Z0-9_]*$`, 3 to 64 characters. A pattern, not a frozen enum |
| `message` | yes | 1 to 2000 characters |
| `request_id` | yes | 1 to 200 characters |
| `details` | no | at most 20 entries, each one of three closed shapes |

A `details` entry is one of three shapes:

- `{pointer, reason}` is the validation shape. `pointer` is at most 512 characters, `reason` at most 2000.
- `{run_id, projected_bytes, projected_runtime_s, projected_cost}` is the body of `409 CONFIRM_REQUIRED`.
- `{recorded_cost, requested_cost, ceiling_cost}` is the body of `409 DAILY_SPEND_CEILING_EXCEEDED`.

A run that answers `409 CONFIRM_REQUIRED` already exists, persisted `awaiting_confirmation`, so the confirmation acts on it rather than repeating the request. `projected_cost` is a decimal string. Money is never a JSON float here. The plan is flat, so that figure is the platform's own infrastructure estimate and is never billed to you.

## The CLI's own envelope

With `--json`, an error is one object on stderr, and the exit code is 2.

```json title="mr-data --json refusal"
{
  "schema_version": "mr-data-error.v1",
  "code": "THIN_STUDIO_RECIPE_DIGEST_MISMATCH",
  "error": "The registered recipe does not have that digest.",
  "headline": "The registered recipe does not have that digest.",
  "remediation": ["…"],
  "retry_after_seconds": 30
}
```

The CLI preserves Studio's code and adds a prefix. A Studio `RECIPE_DIGEST_MISMATCH` reaches a script as `THIN_STUDIO_RECIPE_DIGEST_MISMATCH`, with Studio's own message. A query refusal takes its family as the prefix and drops the repeated word. Studio's `QUERY_TIMEOUT` reaches a script as `THIN_QUERY_TIMEOUT`, never `THIN_QUERY_QUERY_TIMEOUT`. Only a leading `QUERY_` is dropped, so `V4_QUERY_TABLE_UNREACHABLE` reaches a script as `THIN_QUERY_V4_QUERY_TABLE_UNREACHABLE`. Two statuses fold instead of passing through. `401` and `403` both become `THIN_AUTHORIZATION_DENIED`, with Studio's code named in the sentence, and `404` becomes `THIN_NOT_FOUND`.

## Client codes

`mr-data` raises these before or around a request. They carry no HTTP status.

| Code | Raised when | What the author does |
| --- | --- | --- |
| `THIN_UNKNOWN_COMMAND` | the argument list names no command this binary runs | `mr-data --help` lists them |
| `THIN_ARGUMENT_INVALID` | a flag's value is not the shape the flag takes | fix the argument |
| `THIN_REQUEST_INVALID` | the request this command would send is not well formed | fix the input document or flags |
| `THIN_CONFIG_INVALID` | a configured origin is not usable | check `MOSTLYRIGHT_*` environment variables |
| `THIN_AUTHENTICATION_FAILED` | Studio rejected the stored device credential | `mr-data login` |
| `THIN_AUTHORIZATION_DENIED` | the device key does not carry the authority this call needs | check the workspace role. Studio's own code is in the sentence |
| `THIN_SUBSCRIPTION_REQUIRED` | cloud answered 402 | the workspace has no hosted execution access. Not raised while usage is metered: building is then free. |
| `THIN_RATE_LIMITED` | cloud answered 429 | honour `retry_after_seconds` |
| `THIN_TOKEN_EXCHANGE_FAILED` | the CLI could not reach the cloud token exchange, or it returned an expired token | retry. If it persists, cloud's Studio config is wrong |
| `THIN_STUDIO_UNAVAILABLE` | cloud answered 503 across every attempt | retry later |
| `THIN_STUDIO_UNREACHABLE` | Studio did not answer | retry later |
| `THIN_STUDIO_REFUSED` | Studio refused the request with no code in the body | read `http_status` on the refusal |
| `THIN_STUDIO_<CODE>` | Studio refused the request with a v4 code | see [Studio refusal codes](#studio-refusal-codes) |
| `THIN_NOT_FOUND` | Studio has no such resource | check the identifier. A resource in another workspace reads identically |
| `THIN_RESPONSE_INVALID` | the CLI could not parse a reply | client and backend sit on different contract versions |
| `THIN_RESPONSE_TOO_LARGE` | a reply exceeded the byte ceiling for its route (1 MiB for JSON) | narrow the request |
| `THIN_RECIPE_DOCUMENT_UNREADABLE` | the CLI could not read the recipe file | check the path |
| `THIN_RECIPE_DOCUMENT_INVALID` | the recipe file is not strict JSON | a recipe carries integers, strings, booleans, null, objects and lists, and never a fractional number |
| `THIN_CONFIRMATION_MISMATCH` | `--confirm-name` did not match the held name | type the name on the page |
| `THIN_DATASET_VERSION_CONFLICT` | the dataset moved again during a retried rename | `mr-data dataset show`, then rename again |
| `THIN_NO_SEALED_RESULT` | the run has written nothing to read yet | `mr-data watch RUN_ID` |
| `THIN_NO_CHECK_RESULTS` | the run has not finished, so checks have not reported | `mr-data watch RUN_ID` |
| `THIN_NO_PREVIEW_SEALED` | the run composed a multi-part version and wrote no preview | `mr-data query RUN_ID` |
| `THIN_NO_MATCHING_ARTIFACT` | the run wrote no artifact of the requested `--kind` | ask for a kind it wrote |
| `THIN_NOTHING_LIVE` | the table carries a promotion but no succeeded run resolves to a version | build it once successfully |
| `THIN_NO_RELEASED_VERSION` | the run released no version | check `mr-data status` |
| `THIN_NO_FINGERPRINT` | the run's evidence carries no fingerprint | nothing can compare it with another run |
| `THIN_DESTINATION_EXISTS` | the output path holds a different file | a download never overwrites a local file |
| `THIN_ARTIFACT_TOO_LARGE` | the artifact declares more than 16 GiB | not downloadable through the client |
| `THIN_DOWNLOAD_DIGEST_MISMATCH` | the bytes do not hash to the digest Studio declared | retry. Report it if it repeats |
| `THIN_DOWNLOAD_TRUNCATED` | fewer bytes arrived than Studio declared | retry |
| `THIN_DOWNLOAD_OVERRUN` | more bytes arrived than Studio declared | retry |
| `THIN_DOWNLOAD_REFUSED` | the signed download answered non-200 | the session may have expired. Run `download` again |
| `THIN_VERSION_PART_UNREACHABLE` | the version names parts for which Studio created no read URL | a table missing parts is a table missing rows. The CLI reports nothing as downloaded |
| `THIN_QUERY_STATEMENT_TOO_LARGE` | the statement is over 64 KiB | shorten it. The CLI sent nothing |
| `THIN_QUERY_TIMEOUT` | the query outlived its deadline, and Studio failed it with no partial result | narrow the statement (a `WHERE`, fewer columns, a lower `--max-rows`) and ask again. When only the command's own wait runs out, there is no error: it prints `query_still_running` and exits `2`, and the same command reads the answer later |
| `THIN_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 | follow the refusal. 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 |
| `THIN_QUERY_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. When the refusal says the run's table is larger than a bounded query may open, asking again does not help |
| `THIN_QUERY_BINDER_ERROR` | the statement does not bind against the table | check the column names against `mr-data peek` |
| `THIN_STREAM_REFUSED` | Studio refused the event-stream request | retry `watch` |
| `THIN_STREAM_UNAVAILABLE` | the CLI could not re-establish the event stream | the run continues. Resume `watch` |
| `THIN_STREAM_FRAME_INVALID` | a stream frame carried a body that is not JSON | contract mismatch |
| `THIN_SECRET_MATERIAL_INVALID` | a `--part` holds more than the vault stores as one value | shrink the part |
| `THIN_RESEARCH_SESSION_ENDED` | no worker took the session this command opened | retry |
| `THIN_RESEARCH_SOURCE_FORBIDDEN` | the source is not one this session may probe | open the session against the right dataset |
| `THIN_NOTEBOOK_NOT_RENDERED` | Studio renders a notebook when the run finishes | `mr-data watch` until then |
| `THIN_COMMAND_UNAVAILABLE` | this build registers no such command | `mr-data --help` |
| `THIN_COMMAND_HAS_NO_HOSTED_LANE` | the command does not run on the backend | it does not run here |
| `THIN_CLOUD_REFUSED` | cloud refused and stated nothing usable | the sentence quotes what cloud said |
| `THIN_CLOUD_STEP_UP_REQUIRED` | publishing needs a verification only a signed-in browser finishes | open the dashboard and share from that page |
| `THIN_CLOUD_PUBLIC_SHARING_DISABLED` | the organization does not allow public datasets | an owner changes it |
| `THIN_CLOUD_PUBLISHER_PROFILE_REQUIRED` | the organization has no publisher name | set it in team settings |

The full client vocabulary is larger. The `propose` bundle commands, the cancellation journal and the narrative editor each add codes under `THIN_PROPOSE_*`, `THIN_CANCELLATION_*` and `THIN_NARRATIVE_*`. Each states its own sentence and its own fix.

## Studio refusal codes

Studio v4 raises these and answers them as `problem+json`. The message column is Studio's own wording.

### Recipe and registration

| Code | HTTP | Message | What the author does |
| --- | --- | --- | --- |
| `RECIPE_DIGEST_MISMATCH` | 409 | The registered recipe does not have that digest. | pass the digest registration printed |
| `RECIPE_DOCUMENT_NOT_CANONICAL` | 422 | The recipe document cannot be canonicalized. | remove fractional numbers and duplicate keys |
| `RECIPE_DOCUMENT_INCOHERENT` | 422 | The recipe document is internally inconsistent. | read `details[].pointer` |
| `RECIPE_UNAVAILABLE` | 409 | This table has no registered recipe to build from. | register one |
| `DATASET_ID_REQUIRED` | 422 | Create and categorize the dataset first, then put its id in `document.dataset.id`. | `mr-data dataset create` |
| `DATASET_CATEGORY_REQUIRED` | 422 | Choose a primary category with `dataset set --category` before registering a recipe. | `mr-data dataset categories` |
| `DATASET_MISMATCH` | 409 | `dataset.id` names no V4 dataset of this workspace. | check the id |
| `DATASET_NAME_CONFLICT` | 409 | none | choose another name |
| `DATASET_ARCHIVED` | 409 | This recipe's dataset is archived and read-only. | unarchive it |
| `CONTRACT_VALIDATION_FAILED` | 422 | none | the request does not match the v4 contract |
| `SECRET_UNKNOWN` | 409 | This recipe names a secret this workspace has not enrolled. | `mr-data keys set NAME` |
| `SECRET_UNAVAILABLE` | 409 | The secret this source names is no longer enrolled in this workspace. | re-enrol it |
| `CONNECTION_DATASET_REQUIRED` | 409 | Create a dataset and grant it access to this connection, then specify `dataset.id`. | an owner grants it in Settings |
| `CONNECTION_AUTHORITY_MISMATCH` | 422 | Recipe connector authority differs from its saved connection. | re-read the connection and re-register |

### Starting a run

| Code | HTTP | Message | What the author does |
| --- | --- | --- | --- |
| `CLAMPS_REQUIRED` | 422 | A sample run must state at least one clamp. | pass `--max-rows` or `--max-source-bytes` |
| `CONFIRM_REQUIRED` | 409 | This run is over the projection threshold and needs a confirmation before it starts. | re-run with `--confirm`. The projection is in `details` |
| `DAILY_SPEND_CEILING_EXCEEDED` | 409 | none | the amounts are in `details`. Wait or raise the ceiling |
| `RUN_WINDOW_REQUIRED` | 422 | A backfill run must state an exact window. | pass `--window START END` |
| `RUN_MODE_UNKNOWN` | 422 | That is not a v4 run mode. | one of the six modes |
| `RUN_ALREADY_EXISTS` | 409 | The identical run already ended. Register a revised recipe to run again. | revise and re-register |
| `TABLE_RUN_IN_FLIGHT` | 409 | This table already has a mutating run. | wait, or cancel the one in flight |
| `TABLE_BUSY` | 409 | A table with a run still in flight cannot start another. | wait |
| `TABLE_ARCHIVED` | 409 | This run's table is archived and no longer builds. | unarchive it |
| `BUILD_COMPLETE` | 409 | The latest build already read every source whole. There is nothing to run. | nothing to do |
| `REPLAY_SOURCE_REQUIRED` | 422 | A replay must name `sources_from_run`. | pass `--sources-from RUN_ID` |
| `REPLAY_SOURCE_UNAVAILABLE` | 409 | The named replay source is not a succeeded run of this table. | name a succeeded run of the same table |
| `REPLAY_SOURCE_EXPIRED` | 409 | The named run no longer retains its raw snapshot and receipt. | replay is no longer possible for it |
| `REPLAY_WORKER_UNAVAILABLE` | 409 | Replay is unavailable until a compatible worker is deployed. | wait for the deployment |
| `STATE_TRANSITION_INVALID` | 409 | This full run is not awaiting sample approval. | read the run's current state |
| `STEP_UP_BINDING_MISMATCH` | 403 | Approval requires an interactive step-up bound to this full run version. | approve it in the dashboard. A run held behind a PREVIEW does not reach this code: any editor of the workspace releases that one. |
| `ETAG_MISMATCH` | 412 | The run has moved since it was read. | re-read and retry |
| `IDEMPOTENCY_KEY_INVALID` | 422 | `Idempotency-Key` must be 16 to 128 URL-safe characters. | fix the header |
| `SCHEDULE_CADENCE_TOO_FREQUENT` | 422 | That schedule runs more often than this table allows. | slow the cadence |
| `SCHEDULE_CRON_INVALID` | 422 | That cadence is not a satisfiable five-field cron expression. | fix the expression |
| `MANUAL_REFRESH_RATE_LIMITED` | 429 | This workspace has requested 1,200 table refreshes in the current hour. | wait for the hour to turn |

### Reading a table

| Code | HTTP | Message | What the author does |
| --- | --- | --- | --- |
| `NOTHING_TO_PROMOTE` | 409 | This table has no succeeded V4 run to promote. | build it once |
| `TABLE_NOT_PROMOTED` | 409 | This table is not promoted. | the first succeeded run promotes it |
| `TABLE_LIVE` | 409 | A live table cannot be archived. | demote it first |
| `TABLE_VERSION_UNAVAILABLE` | 409 | none | the version is not readable |
| `TABLE_VERSION_ARTIFACT_UNAVAILABLE` | 409 | This V4 table version has no stored artifact of that kind available. | ask for a kind it holds |
| `TABLE_PREVIEW_UNAVAILABLE` | none | none | the run wrote no preview |
| `QUERY_TOO_MANY_PARTS` | none | none | narrow the read with `--field` plus `--from`/`--to`, or `--partition` |
| `TABLE_COMPACTION_INELIGIBLE` | 409 | This table has no bounded adjacent part group to compact. | nothing to compact |
| `TABLE_COMPACTION_RECIPE_CHANGED` | 409 | Compaction must use the recipe that produced its predecessor version. | pass the predecessor's recipe |
| `CURSOR_INVALID` | 422 | The page cursor is not a position in this listing. | start the listing again |

### Streams

| Code | HTTP | Message |
| --- | --- | --- |
| `STREAM_DOCUMENT_NOT_FOUND` | 404 | No registered connector document in this workspace carries that digest. |
| `STREAM_VENUE_UNAVAILABLE` | 503 | This deployment has not configured a continuous stream worker pool. |
| `STREAM_SUBSCRIPTION_MEMBERS_INVALID` | 409 | This stream already feeds as many promoted tables as one subscription may. |
| `STREAM_RECORDING_MEMBERS_INVALID` | 422 | A recordings listing scopes by member ids. |
| `STREAM_RECORDING_REQUEST_INVALID` | 422 | A recordings listing is scoped by one connector document digest. |
| `RECORDING_MEMBERSHIP_CHANGED` | 409 | Recording membership changed between pages: restart from position 0. Source identity changed: one source has two identities across the current tables. Current table page overflowed: the dataset has more than 200 current tables. |
| `RUN_RECORDING_WATERMARK_MISMATCH` | 409 | A stream source recorded a position other than the one it was dispatched. |

### Worker callback codes

An author never sends these. They appear on a failed run's record when Studio refused a worker's callback. `ATTEMPT_STALE`, `ATTEMPT_FENCE_STALE`, `ATTEMPT_SCOPE_MISMATCH`, `ARTIFACT_SESSION_EXPIRED`, `ARTIFACT_KIND_UNKNOWN`, `ARTIFACT_KIND_ALREADY_RESERVED`, `UPLOAD_SESSION_BINDING_MISMATCH`, `SESSION_NOT_FOUND`, `SESSION_EXPIRED`, `CREDENTIAL_HANDLE_UNKNOWN`, `CREDENTIAL_HANDLE_CONSUMED`, `CREDENTIAL_AUTHORITY_STALE`, and the `FETCH_*` family.

Every other v4 code reaches the CLI verbatim as `THIN_STUDIO_<CODE>` with Studio's own message. Studio bounds that message at 2000 characters.

## Worker failure codes

These land on `run.failure_code` when a run fails. A run reports no code outside this list.

### Acquisition

| Code | Meaning | What the author does |
| --- | --- | --- |
| `ACQUISITION_FAILED` | the worker could not fetch a source | read `failure_detail`. Check the address and the publisher |
| `ACQUISITION_FORMAT_UNMAPPED` | the response media type maps to no data format | pin a Reader whose accepted list carries it |
| `ACQUISITION_CHILD_INVALID` | the confined acquisition child did not behave | infrastructure. Report it |
| `SOURCE_HTTP_5XX` | the publisher answered a server error | retry later |
| `SOURCE_RATE_LIMITED` | the publisher rate-limited the fetch | raise `min_interval_seconds`, lower `concurrency` |
| `SOURCE_PROXY_BUSY` | the egress proxy had no room | retry |
| `FETCH_AUTHORITY_UNAVAILABLE` | the worker could not resolve the credential authority | check that the secret or connection remains enrolled |
| `RUN_CREDENTIAL_SCOPE_INVALID` | the credential's scope does not cover this source | re-check `credential` on the source |
| `COLLECTION_DISCOVERY_FAILED` | the publisher refused a first run's listing request | read the coverage block's `discovery_failure` |
| `COLLECTION_MERGE_INVALID` | the collection's pages did not merge | schemas differ between pages |
| `PREDECESSOR_UNAVAILABLE` | the worker could not read the predecessor version | the refresh has nothing to continue from |
| `PREDECESSOR_HOST_REFUSED` | the predecessor's host refused the read | infrastructure |
| `BOUNDED_DIRECT_ROUTE_REFUSED` | the bounded-direct path refused this run | it falls back to batch |

### Clamps and modes

| Code | Meaning | What the author does |
| --- | --- | --- |
| `CLAMP_EXCEEDED` | the run crossed a declared clamp | raise `--max-rows` / `--max-source-bytes`, or narrow the window |
| `CLAMP_UNSUPPORTED_FORMAT` | the clamp does not apply to this source. A document projection cannot run under a sample row clamp | run the projection as a full source |
| `RUN_CLAMPS_REQUIRED` | the mode needs a clamp and the run stated none | pass one |
| `RUN_CLAMPS_INVALID` | the clamps are not a legal set | `--max-rows`, `--max-source-bytes`, `--window` |
| `RUN_MODE_INVALID` | the mode does not fit this run | check the mode against the table's state |
| `RUN_WINDOW_REQUIRED` | a backfill with no window | pass `--window START END` |
| `RUN_RECIPE_INVALID` | the job document's recipe is not executable | read `failure_detail`. It names the source and the reason |
| `JOB_INVALID` | the async job block is not executable | check `generic_api.job` |

### Transform

| Code | Meaning | What the author does |
| --- | --- | --- |
| `TRANSFORM_STATEMENT_NOT_SELECT` | a step is not a single read-only `SELECT` | rewrite the step |
| `TRANSFORM_STATEMENT_NOT_SINGLE` | a step carries more than one statement | split it into steps |
| `TRANSFORM_STEP_DUPLICATE` | two steps share an id | rename one |
| `TRANSFORM_STEP_SHADOWS_SOURCE` | a step id collides with a source name | rename the step |
| `TRANSFORM_COLUMN_TYPE_MISMATCH` | the final `SELECT` returns a type the column declares otherwise | `cast(...)` the column |
| `TRANSFORM_PLAN_INVALID` | the plan is not a runnable graph | check step order and references |
| `TRANSFORM_ENGINE_UNCONFINED` | the worker could not confine the engine | infrastructure |
| `COLUMN_PRESENTATION_INCOMPATIBLE` | a chart does not describe that column type | see [Recipe document](/docs/reference/recipe/) |

### Checks

| Code | Meaning | What the author does |
| --- | --- | --- |
| `CHECK_FAILED` | a declared check with `enforcement: required` failed | `mr-data checks RUN_ID`. Fix the recipe or accept the data |
| `DECLARED_CHECK_INVALID` | a check is not a legal declaration | see [Checks](/docs/reference/checks/) |
| `DECLARED_CHECK_COLUMN_UNKNOWN` | a check names a column the table does not have | fix the name |
| `DECLARED_CHECK_DUPLICATE` | two checks share an id | rename one |
| `DECLARED_CHECK_UNREADABLE` | the worker could not read a check | fix the JSON |
| `DECLARED_CHECKS_INVALID` | the checks block as a whole is not legal | at most 128 checks |

### Windows and snapshots

| Code | Meaning |
| --- | --- |
| `WINDOW_UNRENDERABLE` | the address carries raw braces, or an undeclared placeholder. Window templates are path-only |
| `WINDOW_FORMAT_UNSUPPORTED` | the source arrived in a format no window merges into |
| `WINDOW_MERGE_INVALID` | the slice's columns are not the columns the previous run wrote |
| `WINDOW_PARTITION_UNREADABLE` | a row states no value in `merge.partition.column` that reads as a partition under the declared key |
| `WINDOW_ROWS_OUT_OF_RANGE` | the source returned rows outside the requested window |
| `WINDOW_SPAN_TOO_MANY_PARTITIONS` | the window covers more than 4096 partitions, which is what one merge may replace |
| `SNAPSHOT_COLUMN_RESERVED` | the snapshot column name collides with a publisher column |
| `SNAPSHOT_IDENTITY_INCOMPLETE` | `merge.row_identity` does not carry the snapshot column |
| `SNAPSHOT_PARTITION_MISMATCH` | `merge.partition` is not the snapshot column with `iso_date_value` |
| `SNAPSHOT_PARTITIONS_EXHAUSTED` | the merged relation would hold more days than `max_partitions` declares |
| `SNAPSHOT_SPAN_FIXED` | a snapshot covers one day: `granularity` `day`, `lookback_seconds` 0, `max_span_seconds` 86400 |
| `SNAPSHOT_BEFORE_START_AT` | the worker read the source on a day before the snapshot's `start_at` |
| `SNAPSHOT_REQUEST_EXCLUSIVE` | the window states both `request` and `snapshot` |
| `SNAPSHOT_SCOPES_UNSUPPORTED` | a snapshot window carries `physical_scopes` or `bootstrap` |
| `SNAPSHOT_RELATION_EMPTY` | the fetch decoded to no rows, which is a fetch that returned nothing rather than a day with nothing in it |
| `SNAPSHOT_RELATION_TOO_LARGE` | the merged relation is over 2 GiB |
| `SNAPSHOT_RELATION_TRUNCATED` | a row clamp cut the fetch short, and a snapshot merge writes the whole of its day from that one slice |
| `SOURCE_PHYSICAL_SCOPE_UNKNOWN` | a binding names a scope the run did not request |

### Persist

| Code | Meaning | What the author does |
| --- | --- | --- |
| `ARTIFACT_PERSIST_FAILED` | the worker could not upload an artifact against Studio's signed session | retry the run |
| `RUN_ARTIFACT_SESSION_INVALID` | the artifact session does not match what the worker reported | retry the run |
| `TABLE_PART_OVER_MAX` | one row group carried a part past the part byte ceiling | narrow the window |
| `TABLE_PART_MEMORY_CEILING` | the part sink was holding more bytes than its ceiling allows | narrow the window |
| `TABLE_PART_SCHEMA_DRIFT` | a new part's schema differs from the version's | the recipe's columns changed. Register a revision |
| `TABLE_MANIFEST_INVALID` | the manifest does not describe the parts | retry |
| `TABLE_VERSION_UNCOMMITTABLE` | the worker could not commit the version | retry |
| `TABLE_UPSERT_UNCLUSTERED` | the upsert's key range intersects too many predecessor parts | use `partition_replace`, or cluster the table on its `row_identity` key |
| `TABLE_FORMAT_UNSUPPORTED` | the table format is not one this worker writes | worker and table disagree |
| `TABLE_COMPACTION_INVALID` | the compaction request is not applicable | read `failure_detail` |

### Infrastructure

| Code | Raised by | Meaning | What the author does |
| --- | --- | --- | --- |
| `EXECUTION_LOST` | Studio's lease watchdog | no worker holds this run any more: a crashed process, a preempted job, or an attempt that failed without reporting | re-run it |
| `EXECUTION_LEASE_EXPIRED` | Studio's lease watchdog | a worker still claims it and has stopped saying anything | re-run it |
| `WORKER_TERMINATED` | the worker | something terminated the process | re-run it |
| `RUN_EXECUTION_FAILED` | the worker | the execution failed with no narrower code | read `failure_detail` |
| `RUN_ATTEMPT_INVALID` | the worker | the attempt identity did not check out | retry |
| `RUN_FENCE_INVALID` | the worker | the fence generation did not check out | retry |
| `RUN_PREDECESSOR_INVALID` | the worker | the predecessor document is not usable | retry |
| `RUN_RECORDING_INVALID` | the worker | the named recording is not readable | `mr-data stream recordings --document-digest D` |
| `RUN_WATERMARK_INVALID` | the worker | the stream watermark is not the one dispatched | retry |
| `CLAIM_KIND_UNKNOWN` | the worker | the claim named a kind this worker does not run | worker and Studio disagree |

## Cloud HTTP statuses

Cloud sits in front of Studio for token exchange and hosts the Public Dataset API. What each status means to the CLI:

| Status | Where | Meaning | What the author does |
| --- | --- | --- | --- |
| `401` | cloud or Studio | cloud or Studio rejected the device credential or the token | `mr-data login` |
| `402` | cloud | the entitlement check refused | the workspace has no hosted execution access |
| `403` | Studio | the token does not carry this authority | check the workspace role |
| `409` | Studio | a state conflict, named by the v4 code | read the code |
| `412` | Studio | `If-Match` did not match | re-read the resource and retry |
| `413` | cloud | the artifact or result is too large | narrow the query, or download Parquet |
| `429` | cloud or Studio | rate limited | honour `Retry-After` |
| `503` | cloud | cloud's Studio configuration is incomplete, or Studio stayed unavailable | retry. The CLI retries a 503 that carries a retry window |

## Telling which side answered in 60 seconds

Read the host first. `mostlyright.md` (and `app.`, `api.`) is cloud. A `…run.app` host is Studio. In a CLI error, `/api/auth/*` and `/api/cli/*` are cloud, and `/v4/*` is Studio.

Read the error's shape next. `problem+json` with `schema_version: "4.0.0"` and an `x-correlation-id` is Studio v4. `{schema_version, error, request_id}` is cloud's Public API v2. A JSON-RPC `error.code` is cloud's MCP. A bare `THIN_*` with no HTTP status is the CLI refusing locally.

Read the run record last. `mr-data status RUN_ID --json` carries `failure_code`, `failure_detail` and `failed_stage`. A stage from `acquire, parse, transform, check, persist` with a worker code points at the recipe or the publisher first. `EXECUTION_LOST` and `EXECUTION_LEASE_EXPIRED` point at the platform.

[Fix a failed run](/docs/build/troubleshooting/) walks the common ones. The numeric bounds behind `CLAMP_EXCEEDED`, `READER_BUDGET` and `QUERY_TOO_MANY_PARTS` are on [Limits and ceilings](/docs/reference/limits/).
