Tables
These commands report what a table serves, set its refresh cadence, explicitly resync sources, freeze a version, withdraw it, rename it and retire it.
Under V4 One-Gate, a table’s first succeeded run promotes it, so nothing here is what makes a table readable. promote records the refresh cadence and the reasoning behind it. It is idempotent on a table that is already live, and it is how you put a withdrawn table back.
None of these commands prove that a refresh worker executed. They only change or report control
state. Use mr-data runs and mr-data status to inspect execution records.
mr-data table TABLE_ID [--json]mr-data table resync TABLE_ID --request-id UUID [--json]mr-data table rename TABLE_ID --name NAME [--json]mr-data table archive TABLE_ID [TABLE_ID …] (--confirm DELETE | --confirm-name NAME) [--json]Report what this table is serving and when it refreshes next. The schedule section says what you asked for and why, what the evidence since then settled on, and how many source updates that rests on. Put resync, rename or archive before the table to explicitly reread sources, change its name or retire it.
A second positional separates the bare read from an action. On its own, mr-data table rename returns a refusal rather than reporting on a table called rename.
| Flag | Argument | Required | Meaning | Default |
|---|---|---|---|---|
TABLE|resync|rename|archive |
id or word | yes | The table to report on, or one of the listed actions followed by identifiers. | none |
TABLE … |
ids | with an act | With resync or rename, one table. With archive, every table being retired. The command attempts each archive target whatever the one before it answered. |
none |
--name |
NAME |
with rename |
Lowercase snake_case, at most 63 characters, unique within its dataset. A recipe’s table.name takes the same grammar, so every name here is one a recipe could rebuild. |
none |
--request-id |
UUID | with resync |
One caller-held idempotency key for this full source reread. Choose and retain it before submitting; repeat that exact command only to recover the same request after an ambiguous response. | none |
--confirm |
DELETE |
with archive |
The word DELETE, typed out. It confirms the identifiers you typed rather than a name, so it retires one table or twenty. The comparison is exact, and lowercase delete returns a refusal. |
none |
--confirm-name |
NAME |
with archive |
One table’s current name, typed out. It takes one identifier and rejects more. | none |
--json |
none | no | One JSON object instead of human lines. | off |
Give one confirmation form or the other, never both. Archiving hides the table from the dataset and frees its name. The bytes every run built stay where they are, downloadable and queryable by their run.
{ "schema_version": "mostlyright-thin-client-v4-promotion.v1", "status": "table_reported", "lane": "hosted", "table": { "table_id": "3f8c2d10-6a4b-4c8e-9d1f-5b7a0c2e4d68", "dataset_id": "d31017e0", "name": "daily_city_temperatures", "status": "active", "promotion_status": "live", "live_version_id": "9d4e7a21-0c3b-4f56-8e2a-1b6c5d9f0a37", "pinned_version_id": null, "schedule": "0 3 * * * (UTC)", "schedule_record": { "cron": "0 3 * * *", "timezone": "UTC", "next_trigger_at": "2026-09-13T03:00:00Z" }, "next_refresh_at": "2026-09-13T03:00:00Z", "last_refresh_at": "2026-09-12T03:00:00Z", "last_refresh_run_id": "7c9e1d34-8b52-4f61-9a03-2e5d6f7a8b90", "last_failure": null, "cadence": { "record": {}, "state": "stable", "means": "The refresh schedule is settled and running.", "asked_for": "every 24h", "reason_given": "the station publishes once a day at 02:50Z", "settled_on": "0 3 * * * (UTC)", "settled_because": "…", "updates_seen": 41, "measured_gap": "every 24h", "quickest_source": "asos_hourly", "no_slower_than": "every 24h", "bounded_by_source": "asos_hourly" } }, "evidence": { "source_readings": 82, "readings_that_moved": 41, "last_moved_at": "2026-09-12T02:51:00Z" }}Values are illustrative. The table’s own status is active or archived. A separate promotion_status says whether it is live: unpromoted, live or demoted. schedule flattens the wire’s {cron, timezone, next_trigger_at} into one phrase, and schedule_record carries the object whole beside it. cadence.state is learning, stable, dormant or locked, and means is the sentence for it. The command never prints the token on its own. cadence is null on a table that carries none.
evidence comes from a separate route. When that route cannot answer, the command leaves the block out rather than failing. source_readings counts one reading per source per refresh, not one per refresh. counted_over appears when the page is full, because the listing is also the retention.
When the last refresh failed, last_failure carries the whole {failure_code, failure_detail, failed_stage}. The payload’s note says what that means: live did not advance, so people are reading the version from before it.
{ "schema_version": "mostlyright-thin-client-v4-table-lifecycle.v1", "status": "table_renamed", "lane": "hosted", "table": { "…": "…" }, "name": "new_name", "previous_name": "old_name", "retried_after_conflict": false, "note": "The name old_name is released, and another table in this dataset may take it."}The released-name sentence appears only when the rename gave a name up. Renaming a table to the name it already has changes nothing and leaves the sentence out.
{ "schema_version": "mostlyright-thin-client-v4-table-batch.v1", "status": "tables_partly_archived", "lane": "hosted", "counts": { "requested": 2, "archived": 1, "refused": 1, "unreachable": 0 }, "outcomes": { "table 1": "…", "table 2": "…" }, "tables": [ { "table_id": "3f8c2d10-6a4b-4c8e-9d1f-5b7a0c2e4d68", "outcome": "archived", "name": "old_table", "code": null, "said": "…", "table": {} }, { "table_id": "tbl_02J…", "outcome": "refused", "name": null, "code": "THIN_STUDIO_TABLE_LIVE", "said": "…", "table": null } ], "note": "…", "confirmed_by": "--confirm DELETE"}status is tables_archived, tables_partly_archived or tables_not_archived, one word each for all of them, some of them and none of them. The command attempts each table whatever the one before it returned. outcome reads refused when Studio returned no, and unreachable when no answer arrived at all. A missing response can follow a committed mutation, so read the table state before retrying. The command exits 1 when refused + unreachable is non-zero.
Errors on archive: THIN_STUDIO_TABLE_LIVE (withdraw it with demote first), THIN_STUDIO_TABLE_BUSY (a run against it is in flight), THIN_STUDIO_TABLE_ARCHIVED, THIN_CONFIRMATION_MISMATCH. On rename: THIN_REQUEST_INVALID for a name outside the grammar or more than one identifier, THIN_DATASET_VERSION_CONFLICT when the dataset moved twice under the retry.
resync
Section titled “resync”mr-data table resync TABLE_ID --request-id UUID [--json]Resync is the explicit whole-source operation. It is for a corrected recipe, another snapshot, an unwindowed source, or known historical repair. It is not a way to make a normal refresh “try harder”: normal and scheduled refreshes remain incremental and return Studio’s public RESYNC_REQUIRED refusal before acquisition when there is no normal-refresh action or bounded materializer.
The response carries the run that Studio admitted, the caller-held request_id that makes a retry idempotent, and idempotent_replay: true only when Studio returned the run already admitted for that same key. A new request id means a deliberate new resync. A resync uses one progressive full build, with any required confirmation before acquisition. Its inspection checkpoint continues automatically without a separate sample or second full run. Existing legacy sample/full pairs remain readable and approvable. The resync rereads every source, rather than carrying an incremental refresh action plan.
mr-data table "$TABLE_ID" --json | jq '{serving: .table.live_version_id, next: .table.next_refresh_at}'mr-data demote "$TABLE_ID"mr-data table archive "$TABLE_ID" --confirm DELETEThe cadence grammar
Section titled “The cadence grammar”promote --cadence and reschedule --cadence take one of four forms:
| Form | Example | Meaning |
|---|---|---|
| A five-field cron expression | 0 3 * * * |
Refresh at these times. |
| An interval | every 6h |
Refresh this often. The units are m, h and d. The count is 1 to 6 digits. |
| An anchored interval | every 30m from 2026-09-03T12:20:00Z |
The same, phased from that moment. The anchor is YYYY-MM-DDTHH:MM:SSZ, optionally with 1 to 9 fractional-second digits, and a literal uppercase T and Z. |
The word source |
source |
Let Studio choose from what the source does. promote only. reschedule refuses it, because it asks for the schedule to move to rather than a starting point. |
every, from and the unit letter are case-insensitive. The anchor’s T and Z are not, because the contract refuses a timestamp with a lowercase z.
Whatever you give is a starting point. Studio keeps moving the schedule to match what it sees the source do, unless you freeze it with reschedule --lock. On promote, every form except source requires --why. A reason offered beside source returns a refusal instead of being ignored. On reschedule it is never required, only bounded. --why is at most 2000 characters.
promote
Section titled “promote”mr-data promote TABLE_ID [--cadence CADENCE] [--why TEXT] [--no-activity] [--json]Record how often this table refreshes and the reasoning behind it. A table’s first succeeded run goes live on its own, so this command is not what makes it readable. It is idempotent on a table that is already live, and it is how you put a withdrawn table back.
| Flag | Argument | Required | Meaning | Default |
|---|---|---|---|---|
TABLE |
id | yes | The table, by the identifier mr-data recipe printed when it registered one. |
none |
--cadence |
cadence | no | The refresh schedule to start from. See The cadence grammar. | none |
--why |
TEXT |
with a stated cadence | Why this schedule fits the source, in a sentence or two. It sits beside the table for the next person to read. | none |
--no-activity |
none | no | Do not tell the dataset page what this command is doing. | off |
--json |
none | no | One JSON object instead of human lines. | off |
{ "schema_version": "mostlyright-thin-client-v4-promotion.v1", "status": "table_promoted", "lane": "hosted", "table": { "…": "the same projection mr-data table prints" }, "note": "Studio accepted the publication request and reports the resulting table state below. This response alone does not prove idle-period catch-up or scheduled refresh executed."}Errors:
| Code | When |
|---|---|
THIN_STUDIO_NOTHING_TO_PROMOTE |
There is no succeeded run to make live yet. Start one with mr-data run --recipe … --digest … --full, and mr-data checks tells you whether it passed. |
THIN_STUDIO_TABLE_ARCHIVED |
This table is retired, so nothing can make it live. Nothing built was deleted. To publish this work, register a recipe naming a new table in the same dataset and promote that one. |
THIN_REQUEST_INVALID |
A cadence outside the grammar. A --why beside --cadence source. A --why over 2000 characters. A stated cadence with no --why. |
mr-data promote "$TABLE_ID" --cadence "every 24h" --why "the station publishes once a day at 02:50Z"reschedule
Section titled “reschedule”mr-data reschedule TABLE_ID --cadence CADENCE [--why TEXT] [--lock | --unlock] [--json]Change the refresh schedule for this table. This builds nothing and spends nothing. The next refresh happens at the new time.
| Flag | Argument | Required | Meaning | Default |
|---|---|---|---|---|
TABLE |
id | yes | The table. | none |
--cadence |
cadence | yes | The refresh schedule to start from. See The cadence grammar. | none |
--why |
TEXT |
no | Why this schedule fits the source. Never required here, unlike on promote. |
none |
--lock |
none | no | Hold this schedule and stop adjusting it from what the source does. | off |
--unlock |
none | no | Let the schedule follow what the source does again. | off |
--json |
none | no | One JSON object instead of human lines. | off |
--lock and --unlock are mutually exclusive.
{ "schema_version": "mostlyright-thin-client-v4-promotion.v1", "status": "table_rescheduled", "lane": "hosted", "table": { "…": "the same projection mr-data table prints" }}After a --lock, table.cadence.state reads locked and means reads “You fixed this schedule, so it is left alone.”
Errors:
| Code | When |
|---|---|
THIN_REQUEST_INVALID |
--cadence source, which asks Studio to pick a starting point and belongs on a table’s first publication. A cadence outside the grammar. An anchor whose digits name no moment. An interval count of zero. A --why over 2000 characters. |
THIN_STUDIO_* |
The table’s own refusals, carried through under Studio’s codes. That includes the version conflict when somebody else moved the schedule between the read and the write. |
promote’s two other THIN_REQUEST_INVALID causes cannot arise here. A stated cadence needs no --why, and the gate refuses source before it reads the reason.
mr-data reschedule "$TABLE_ID" --cadence "0 3 * * *" --why "the publisher posts overnight" --lockmr-data pin TABLE_ID --version VERSION_ID [--json]Freeze what people are reading, at a version you name. Use it when a source starts publishing something you do not want served yet. Use it also to hold a version steady while you check something downstream against it.
| Flag | Argument | Required | Meaning | Default |
|---|---|---|---|---|
TABLE |
id | yes | The table. | none |
--version |
VERSION_ID |
yes | The version to freeze at. A succeeded run’s record carries its version identifier, and mr-data status RUN_ID prints it. |
none |
--json |
none | no | One JSON object instead of human lines. | off |
{ "schema_version": "mostlyright-thin-client-v4-promotion.v1", "status": "table_pinned", "lane": "hosted", "table": { "pinned_version_id": "9d4e7a21-0c3b-4f56-8e2a-1b6c5d9f0a37", "live_version_id": "9d4e7a21-0c3b-4f56-8e2a-1b6c5d9f0a37", "…": "…" }}On a pinned table, pinned_version_id is the version people are reading, and a bare mr-data parts lists it.
Errors: THIN_REQUEST_INVALID when --version is missing, and the refusal says where to find one. THIN_NOT_FOUND for a version this table does not hold.
mr-data pin "$TABLE_ID" --version "$VERSION_ID" --jsonmr-data unpin TABLE_ID [--json]Remove the pin and report the live-pointer state that comes back. It does not prove that a refresh ran or that a newer version exists. It lifts the freeze, and the schedule decides what people read next.
| Flag | Argument | Required | Meaning | Default |
|---|---|---|---|---|
TABLE |
id | yes | The table. | none |
--json |
none | no | One JSON object instead of human lines. | off |
{ "schema_version": "mostlyright-thin-client-v4-promotion.v1", "status": "table_unpinned", "lane": "hosted", "table": { "pinned_version_id": null, "live_version_id": "tv_02J…", "…": "…" }}Errors: THIN_STUDIO_* for a table with no pin to remove.
mr-data unpin "$TABLE_ID" --jsondemote
Section titled “demote”mr-data demote TABLE_ID [TABLE_ID …] [--json]Withdraw the live pointer from each table you name and report the schedule state each one returns. Built versions stay readable and stay downloadable by their run. Serving stops.
Naming several tables withdraws them one after another. The command attempts every one whatever the one before it answered, prints each on its own line, and exits non-zero if any is still live.
| Flag | Argument | Required | Meaning | Default |
|---|---|---|---|---|
TABLE … |
ids | yes | One table or several. | none |
--json |
none | no | One JSON object instead of human lines. | off |
{ "schema_version": "mostlyright-thin-client-v4-table-batch.v1", "status": "tables_demoted", "lane": "hosted", "counts": { "requested": 2, "demoted": 2, "refused": 0, "unreachable": 0 }, "outcomes": { "table 1": "…", "table 2": "…" }, "tables": [{ "table_id": "3f8c2d10-6a4b-4c8e-9d1f-5b7a0c2e4d68", "outcome": "demoted", "name": "…", "code": null, "said": "…", "table": {} }], "note": "…"}status is tables_demoted, tables_partly_demoted or tables_not_demoted. The command exits 1 when refused + unreachable is non-zero.
Errors per table: THIN_STUDIO_TABLE_BUSY when a run against this table is in flight. The live pointer has not moved yet, so follow the run with mr-data watch. THIN_STUDIO_TABLE_ARCHIVED when the table is retired, so there is no live pointer to withdraw.
mr-data demote "$TABLE_A" "$TABLE_B" --json | jq '.counts'See also
Section titled “See also”- CLI reference: conventions, exit codes, environment variables
- Reading a table:
partslists what the live or pinned version holds - Runs: the runs that build the versions these commands point at