# Publish and keep it fresh

Source: https://mostlyright.md/docs/build/publish-and-refresh/

The first successful run makes a table live. You decide how often it refreshes and who can read it.

## The first succeeded run goes live

Studio promotes a never-promoted table on `run_succeeded`, whatever the mode. A complete sample counts. Studio dispatches no catch-up run for it. Everything before that point is workspace scratch. Probing, building, downloading and interrogating need no approval step. The only pause is a confirmation on a large run. Nothing has to run to enable the table.

A run never makes a demoted or archived table live again, and `mr-data promote` is how such a table returns. A promotion is also not evidence that a schedule is executing or that an idle period was filled.

## Record the cadence

```bash
mr-data promote TABLE_ID --cadence "every 6h from 2026-09-03T02:40:00Z" --why "…" --json
```

| Flag | What it does |
| --- | --- |
| `TABLE` | The table, by the identifier `mr-data recipe` printed when it registered one. |
| `--cadence CADENCE` | The refresh schedule to start from: a five-field cron expression, an interval such as `every 6h` or `every 30m from 2026-09-03T12:20:00Z`, or the word `source` to let Studio choose. |
| `--why WHY` | Why this schedule fits the source, in a sentence or two. Required unless you asked for `source`. |
| `--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. |

This does not make the table readable. It records how often the table refreshes and why. It is idempotent on a table that is already live, and it is how a withdrawn table comes back.

Say what the source does: the times of day it posts and how often it changes. Say how long after the stated publication time the bytes appear. `--why` sits beside the table for whoever looks next.

The cadence is a seed, not a setting. Studio watches what each source does on every refresh and moves the schedule to match, so it corrects a wrong seed rather than obeying it.

The answer carries Studio's state for the table and any continuation cursors. Studio accepting the schedule does not prove an idle period was caught up or that a scheduled refresh executed.

## How the cadence is learned

The deployed mechanism is the v4 refresh loop. The worker compares the sha256 of each source's acquired bytes against the predecessor receipt's digest for that source. It reports one observation per source on `run:succeeded`, and it stops before the transform when nothing moved. Studio turns those observations into a schedule and applies it without an agent.

Three words mean different things.

- Poll cadence is when Studio chooses to probe a source.
- Publication cadence is the interval observed between digest-bound material changes.
- Publication delay is the interval between the publisher's own stated publication time and the first observation of that edition.

A schedule is not settled until the table says it is. `mr-data table TABLE_ID` reports the state in plain words, the schedule in force, the next refresh, and what was asked for and why. Once there is one, it also reports the interval Studio measured and how many source updates that rests on. Never describe a schedule as learned, settled or confirmed while that command says otherwise. Detection time is not publication time. A plain HTTPS source that cannot prove how many editions a publisher released between two probes stays in a learning state however long it runs.

A `window.snapshot` source partitions a whole-source acquisition on the run's own day. It has no
normal-refresh action, so Studio does not use the refresh loop to acquire later snapshots. Use an
explicit resync when you deliberately want to read that current state again. The one exception is
under [Needs server 4.14](#needs-server-414).

## Change the schedule without building anything

```bash
mr-data reschedule TABLE_ID --cadence "every 12h" --why "…" --lock --json
```

| Flag | What it does |
| --- | --- |
| `TABLE` | The table. |
| `--cadence CADENCE` | Required. Same grammar as `promote`. |
| `--why WHY` | Why this schedule fits the source. |
| `--lock` | Keep exactly this schedule and stop adjusting it from what the source does. |
| `--unlock` | Go back to letting the schedule follow what the source actually does. |
| `--json` | One JSON object instead of human lines. |

Nothing is built and nothing is spent. The next refresh happens at the new time. A live table's schedule can change this way without the demote-and-promote that would reset its bookkeeping and buy a catch-up run. A second `promote` on a live table is an idempotent replay that ignores the new cadence.

`--lock` is how a person overrides the evidence. Do not lock a schedule on your own judgement.

## What a refresh actually does

A refresh runs only the next declared incremental slice from the last successful run. It never silently widens to a whole-source read.

Before it is admitted, Studio writes one predecessor-bound action for every source. The action is one of `reuse_predecessor`, `acquire_incremental`, or `recorded`. The run record carries that action plan, so the projected bytes describe the sources this run will acquire, not every source the recipe has ever used.

- A source with a declared incremental request window asks only for the new range and its stated lookback. Its merge replaces the affected partitions and preserves every other row.
- A closed source reuses the bytes and rows its predecessor sealed.
- A recorded stream continues after the bookmark its predecessor committed. It rewrites no earlier segment.
- A source with no normal-refresh action, including a snapshot or unwindowed source, is refused with `RESYNC_REQUIRED`. Nothing is acquired, and the table continues serving its current version.

Studio also returns `RESYNC_REQUIRED` when the action shape has no bounded materializer. A normal
refresh materializes an all-recorded stream plan, or a plan carrying at least one direct request
window. A closed source sits beside those windows and reuses its sealed bytes. Several windows are
allowed when they agree on the materialization and the partition layout. Where the plan names more
than one source, each window's `merge.partition.column` has to be a column the table declares.

A per-row request collection can join those windows. Its upstream has to be one of them, merged
with `partition_replace`, and the collection names that window's `merge.partition.column` in
`carry_columns`. It then refreshes with its window. The refresh derives the requests of the
partitions the window replaces, plus any partition where the last run left a request failed or
pending, and sends only the ones the predecessor did not answer. A plan holding such a collection
and a closed source is refused.

A closed-only plan, a page collection, a snapshot source, and a window beside a recorded stream
each require an explicit resync or a declared `rebuild`. These checks happen before acquisition.

`mr-data recipe explain RECIPE.json` lists every reason a recipe can or cannot refresh, each with
a rule code and Studio's suggested repair. `mr-data recipe explain RECIPE_ID` does the same for the
revision a table runs now, against its sealed predecessor. It exits `2` when a reason blocks the
refresh.

Only a source explicitly declared `closed: true` authorizes `reuse_predecessor` in a strict refresh plan. A `304` is an unchanged reply after Studio acquires a source, and the legacy settled-source heuristic does not authorize reuse in that plan. A refresh whose plan acquires no rows ends `unchanged`, writes no version, and uploads nothing.

## Rebuild a table that cannot refresh

A source that serves only its current state, such as an offset-paginated listing, a latest dump or
a file rewritten in place, has nothing to fetch less of. `rebuild` says so, and it is the only
consent to spend on a scheduled whole read that Studio takes.

```json
"rebuild": {"mode": "whole_table", "max_rebuild_bytes": 270000000, "min_interval_seconds": 604800}
```

| Member | What it is |
| --- | --- |
| `mode` | `whole_table`, the only value. The rebuild is an ordinary `full` run: it reads every source whole and replaces the table version. |
| `max_rebuild_bytes` | The standing answer to the spend confirmation a scheduled run has no client to give. A rebuild starts only while its projected acquisition fits inside it. A projection past it is refused `REBUILD_BUDGET_EXCEEDED` before any run exists. |
| `min_interval_seconds` | 3,600 to 31,536,000, the shortest gap between two rebuilds. Studio applies the larger of it and the platform refetch floor. |

Set `max_rebuild_bytes` at one and a half times the bytes a measured full acquired: the sum of
`bytes_fetched` over its receipt's sources, or `totals.projected_bytes` in
`mr-data recipe forecast RECIPE_ID --digest DIGEST`. The example's 270,000,000 is 1.5 times a
measured 180 MB.

A table that can refresh incrementally keeps doing so, and the declaration changes nothing for it.
A rebuild is a `full` run, so it reuses the per-row answers a full reuses. Registration refuses
`rebuild` beside a recorded stream or a snapshot source with `REBUILD_STRATEGY_INVALID`, because a
whole read would drop what those sources hold.

A rebuild that fails on a platform fault, such as `EXECUTION_LOST` or `WORKER_TERMINATED`, is retried
up to three times, after 15 minutes, 1 hour and 4 hours. Each retry resumes the failed run. A refusal before any run exists, such as `REBUILD_BUDGET_EXCEEDED` or
`DAILY_SPEND_CEILING_EXCEEDED`, is not retried.

`mr-data table TABLE_ID` prints the strategy Studio reports: `rebuilds every 7d; next T`, a pending
retry as `retry 1/3 at T`, or `held: BLOCKER` for a live table with no refresh and no rebuild.
`mr-data reschedule` on a held table answers `table_schedule_held`, exits `2` and names the two
repairs: declare `rebuild`, or run `mr-data table resync`. It never reports a held table as fixed.

## Resync from sources is explicit

Use **Resync from sources** only when you intend to read source material again — for example after correcting a recipe, acquiring another snapshot, advancing an unwindowed source, or repairing known historical data. It is a separate operation, has its own idempotency key and spends through the same confirmation gate as any other large run. A resync can read a whole source; a normal or scheduled refresh cannot.

The dashboard offers this control only after an incremental refresh is refused. When a resync reaches the confirmation gate, it says that the operation re-reads every source and shows Studio's projected data and runtime. It never changes a **Refresh** or **Refresh all** click into a resync.

## Needs server 4.14

Check first: run `mr-data recipe explain RECIPE.json --json` and read `studio_capabilities`. Use a
feature below only when its capability is true. When it is false, the feature is not available on
this Studio yet, and a snapshot table keeps the one day its last full run read.

With `snapshot_refresh` true, a snapshot table keeps a daily history. A refresh of a snapshot
source appends the UTC day observed in the receipt's `fetched_at` onto the raw relation the
predecessor sealed. Earlier days survive, and a second reading on the same day replaces that day.
The cadence floor is daily, and a missed UTC day is a hole nothing repairs.

The history ceiling is fixed at registration, and raising it discards the history. Size
`window.snapshot.max_partitions` and `limits.max_source_bytes` for the whole history you mean to
keep when you register, up to the 4 GiB merge ceiling. A refresh whose merged history would pass
`limits.max_source_bytes` fails `SNAPSHOT_HISTORY_EXCEEDS_SOURCE_CEILING` before it seals, and the
live version stays readable. `mr-data recipe forecast` reports `days_remaining` and warns
`SNAPSHOT_HISTORY_CEILING_NEAR` at 30 days.

Once a snapshot table holds history, Studio refuses every `full` and `sample` of it with
`409 SNAPSHOT_HISTORY_WOULD_BE_DISCARDED`. That covers `mr-data run`, `mr-data table resync`, the
dashboard's Build full, and the full a manual refresh falls back to. `--discard-snapshot-history`
on `mr-data run --full` or `mr-data table resync` replaces every held day with one. The discarded
days cannot be fetched again.

With `fresh_read_control` true, `--no-saved-answers` on `mr-data run --full` or
`mr-data table resync` starts a full that reuses no per-row answer saved by an earlier full, and
pays for every call again. A retry of that run reads fresh too.

## Live pointer, immutable version, pin

Every run that succeeds writes an immutable table version. The live pointer names which of those versions the public page, the Public Dataset API and the MCP tools read. It tracks the latest passing version by default, and a failed refresh never advances it.

```bash
mr-data pin TABLE_ID --version VERSION_ID --json   # freeze the pointer for a rollback or a hold
mr-data unpin TABLE_ID --json                      # resume tracking
```

`--version` takes the version identifier, never the `table_version_number` printed beside it. Both commands report the resulting pointer state. Read that state rather than inferring from the command name. Neither proves a refresh is available or producing new versions.

Reproducible reading pins the version too. Record `table_version_id` and `manifest_digest` out of `mr-data parts TABLE_ID --json` and pass `--version` from then on.

## Read what a table is serving

```bash
mr-data table TABLE_ID --json
```

Reports what the table is serving and when it refreshes next. It also reports what was asked for and why. It reports what the evidence since then settled on, and how many source updates that rests on. Reading this state does not prove a refresh worker executed.

## Rename and retire

```bash
mr-data table rename TABLE_ID --name lkpr_daily_temperature --json
```

`--name` is lowercase snake_case, at most 63 characters, and unique within its dataset. It follows the same rules as a recipe's `table.name`, so the renamed table remains reproducible. Renaming does not rebuild the table. Every run, version and schedule remains attached to the same identifier. The write fences on the dataset's version and retries once if somebody else wrote first.

Retiring is two commands rather than a loop, because a live table cannot be archived:

```bash
mr-data demote TABLE_ID TABLE_ID ... --json
mr-data table archive TABLE_ID TABLE_ID ... --confirm DELETE --json
```

`demote` withdraws the live pointer and any schedule the table owns. `archive` hides the table from the dataset and frees its name.

| Flag | What it does |
| --- | --- |
| `--confirm DELETE` | With `archive`, the word `DELETE`, typed exactly and nothing else. It confirms the identifiers you typed, so it retires one table or twenty, each under the name Studio is holding for it. |
| `--confirm-name NAME` | With `archive`, one table's current name, typed out. It confirms which table you mean, so it takes one identifier and the client refuses more. |

Neither command deletes anything. Every version stays downloadable and queryable by its run. Neither command stops at the first refusal. Both attempt every table, print one outcome line for each, and exit non-zero if any did not move. Read the per-table outcomes, which `--json` carries as an array with counts beside it, rather than the exit code alone. Both refuse a table a run is still writing to until the run has finished.

## Who can read the dataset

Going live is about the table. Who can see it is about the dataset.

```bash
mr-data dataset publish DATASET_ID --mode public --json
mr-data dataset publish DATASET_ID --show --json
```

| `--mode` | What it means |
| --- | --- |
| `public` | Listed in the public directory and served at an address anybody can read. The default when the flag is left off. |
| `link` | Served at an unlisted address, to whoever holds that address. |
| `private` | Back to the workspace. |

`--show` reports who can read the dataset now and changes nothing.

`THIN_CLOUD_PUBLIC_SHARING_DISABLED` means the organization does not allow public datasets. `THIN_CLOUD_PUBLISHER_PROFILE_REQUIRED` means the organization has no publisher name yet, and a public dataset is published under one. `THIN_CLOUD_STEP_UP_REQUIRED` means publishing needs a verification only a signed-in browser can finish. Open the dashboard address the refusal names and share it from that page.

## What readers see

A public dataset page shows the dataset's title, its primary category and topics, its description and its cover image. It also shows the sources card, the decision record, and each table's live version with its schema and a preview.

The same tables answer the Public Dataset API and the MCP tools. Anonymous callers read dataset identity, the tables, each table's schema and a bounded preview. A key lifts the query page size. The routes are in [Public Dataset API](/docs/reference/public-dataset-api/), and [For agents](/docs/guides/for-agents/) is the copy-paste ladder through them.

The table serves only the live version. A pinned table serves the pinned version. A demoted table serves nothing, though every version it built stays readable by its run.

## What you may claim, and what you may not

`promote` and `reschedule` record a schedule. They are not proof of data continuation. The worker executes a supported refresh, including declared source windows. It does not make a frozen date, a generic cursor or a file listing advance by inference. It does not attach or execute a warehouse schedule.

Another deployed component may do that work. Only its returned state and durable run evidence support a claim that a table is live, caught up, or refreshing. Where that evidence is absent, say that a schedule was recorded and that ongoing freshness is unavailable. Do not imply that recording it filled the gap.

## Next

[Live streams](/docs/build/live-streams/) for venues that have to be recorded before a build can read them. [Fix a failed run](/docs/build/troubleshooting/) for a refresh that stopped.
