Recipes and sources
Find a source with catalog, question it with probe, store its credential with keys, then register the whole recipe document with recipe.
recipe
Section titled “recipe”mr-data recipe FILE [--no-activity] [--json]mr-data recipe show RECIPE_ID [--json]mr-data recipe readiness [--json]mr-data recipe registers one whole recipe document and prints the identifiers the server derived: the recipe, its digest, the dataset, the table and the sources. Identical bytes converge on one recipe by digest, so registering the same file twice is idempotent. A changed document is a revision against the same dataset and table.
Registration is the slowest step a person watches, so the command tells the dataset page it is drafting before it sends. Write the document first (recipe reference), register it, then start a run with the identifier and digest this prints.
The positional is a path to a JSON file, or the word show followed by a recipe identifier. recipe show on its own refuses by name rather than registering a file called show. Name such a file with a directory in front of it.
| Flag | Argument | Required | Meaning | Default |
|---|---|---|---|---|
FILE |
path or show |
yes | The recipe document to register, or the word show. |
none |
RECIPE_ID |
id | with show |
The recipe to read back. | none |
--no-activity |
none | no | Do not tell the dataset page this command is drafting. | off |
--json |
none | no | One JSON object instead of human lines. | off |
This client checks the cheap half before it sends. The file parses as strict JSON, it is one object and it is at most 1 MiB. It carries dataset, question, table, sources, transform, checks, units and timezone. Studio validates the whole document against recipe.schema.json.
{ "schema_version": "mostlyright-thin-client-recipe.v1", "status": "recipe_registered", "lane": "hosted", "recipe_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "recipe_digest": "9f3c2b…", "dataset_id": "d31017e0", "table_id": "3f8c2d10-6a4b-4c8e-9d1f-5b7a0c2e4d68", "source_ids": ["2c7d5b18-9e04-4a63-8b1d-6f3a9c0e5d72", "8b1f0a63-4d29-4e75-9c08-3a6d2b7f1e40"], "columns_without_description": 3, "warnings": { "warning 1": "…", "warning 2": "…" }, "idempotent_by": "recipe_digest"}Values are illustrative. A clean registration carries no columns_without_description and no warnings. recipe show answers schema_version: "mostlyright-thin-client-recipe-read.v1", status: "recipe_reported", the same five identifiers, and document carrying the registered document verbatim.
Errors:
| Code | When |
|---|---|
THIN_RECIPE_DOCUMENT_UNREADABLE |
The file could not be read, or runs over 1 MiB. |
THIN_RECIPE_DOCUMENT_INVALID |
Not strict JSON, not one object, or missing a required member. The refusal names the path, the reason and the member rather than handing back a 422. A recipe document carries integers, strings, booleans, null, objects and lists, and never a fractional number. |
THIN_REQUEST_INVALID |
recipe show with no identifier, or a second positional after a file. |
THIN_RESPONSE_INVALID |
Studio registered a recipe without one of the five identifiers, or without any source identifiers. |
THIN_STUDIO_* |
Studio’s own schema validation and contract refusals, carried through under their own codes. |
REG=$(mr-data recipe recipe.json --json)mr-data run --recipe "$(jq -r .recipe_id <<<"$REG")" \ --digest "$(jq -r .recipe_digest <<<"$REG")" \ --sample --max-rows 500 --jsonrecipe readiness
Section titled “recipe readiness”mr-data recipe readiness [--json]Read every recipe revision in the active workspace and classify whether its current table can refresh incrementally. This is an inventory read: it starts no run and changes no recipe. Each row names the immutable recipe revision, whether it is current, the table’s live state, refresh_ready, every source’s classification (closed, window, collection, recorded, or resync_only), and the names blocking incremental refresh.
Use it before a broad recipe migration. A refresh_ready: false row is not permission to retry a whole source on the schedule; correct its recipe or use the explicit table resync operation when a full reread is intentional.
mr-data probe SOURCE QUESTION --dataset DATASET [--kind {source_inspect,sample_rows,profile_columns,evaluate_expression}] [--request JSON | --request-file FILE] [--json]mr-data probe asks one source one question through a warm worker, before a recipe commits to it. It opens or reuses a session and closes it, so there is no session to name. It closes the session it opened even when the probe is refused.
Use it to learn a source’s shape, its columns and its first rows, while the table is still undecided. source_inspect, the default, reports the shape and no cell values.
Two limits. The command reaches the frozen v3 session routes, and a deployment that has switched them off refuses it. It also takes a question identifier, which no v4 registration receipt returns. On a hosted-only workspace, register the recipe, take an unwindowed sample under --max-rows, then run peek, query and receipt. Probe a source walks through that.
| Flag | Argument | Required | Meaning | Default |
|---|---|---|---|---|
SOURCE |
id | yes | The source to ask about, by its identifier. | none |
QUESTION |
id | yes | The question being explored, by its identifier. A session is bound to one. | none |
--dataset |
DATASET |
yes | The dataset the source and the question belong to. | none |
--kind |
one of four | no | What to ask. source_inspect reports shape only. sample_rows needs a limit in the request body. profile_columns and evaluate_expression take their own bodies. |
source_inspect |
--request |
JSON object | no | The rest of the request body as one JSON object. The source is filled in from SOURCE. |
none |
--request-file |
FILE |
no | The same body read from a JSON file instead of the command line. | none |
--json |
none | no | One JSON object instead of human lines. | off |
{ "schema_version": "mostlyright-thin-client-probe.v1", "status": "probe_answered", "lane": "hosted", "source_id": "2c7d5b18-9e04-4a63-8b1d-6f3a9c0e5d72", "question_id": "6a0d3e91-7c58-4b2f-8d16-4e9b0c3a7f52", "dataset_id": "d31017e0", "probe_kind": "source_inspect", "answer": {}, "session": { "reused": true, "closed": true }, "not_evidence": "…"}Values are illustrative. status is probe_answered when the worker answered, and probe_settled when it settled without an answer, having failed, expired or been cancelled. On probe_settled, answer is null, failure_code carries the worker’s code when there is one, and the command exits 2.
Errors: THIN_RESEARCH_SESSION_ENDED and THIN_RESEARCH_SOURCE_FORBIDDEN. THIN_SESSION_BINDING when Studio answered about a different session, probe or session run than the one asked about, which says nothing about the session’s own state. THIN_REQUEST_INVALID for a --request that is not one JSON object, or a --kind that needs a body and got none. A workspace already at its bound of live research sessions refuses with retry_after_seconds on the refusal.
mr-data probe "$SOURCE_ID" "$QUESTION_ID" --dataset "$DATASET_ID" --kind sample_rows \ --request '{"limit": 20}' --jsoncatalog
Section titled “catalog”mr-data catalog search QUESTION [--format FORMAT] [--limit N] [--json]mr-data catalog search asks the public-source catalogue one question and reads back the entries it ranks against it, best first. Every ranked entry carries the disposition its own facts earned, an entry the catalogue could not vouch for included.
It fetches nothing and registers no source. The answer is discovery evidence over one provider’s partial sweep, and it is never exhaustive. Search elsewhere too.
| Flag | Argument | Required | Meaning | Default |
|---|---|---|---|---|
search |
none | yes | The one verb. It asks the catalogue and ranks what it holds. | none |
QUESTION |
text | yes | What the data has to be about, in plain language. The grain, the place and the period, not a filename. | none |
--format |
csv, json, … |
no | The one data format this question requires. It states the requirement and does not filter. An entry that does not declare the format still comes back, ranked, with disposition: "refused" and filters_match: false. The catalogue accepts one format per question. |
none. No entry is rejected for its format alone, and most entries declare none |
--limit |
1 to 25 |
no | How many entries to rank. | 10 |
--json |
none | no | One JSON object instead of human lines. | off |
{ "schema_version": "mostlyright-thin-client-v4-catalog-search.v1", "status": "catalog_searched", "lane": "hosted", "question": "daily maximum temperature by US city since 2020", "incomplete_code": null, "requirement": { "data_format": "csv" }, "generation": { "store_digest": "9f3c…", "entry_count": 262144 }, "work": { "ranges_scanned": 12, "ranges_pruned": 40, "candidates_examined": 900, "member_bytes_read": 811000 }, "candidates": { "source 1": { "rank": 1, "title": "…", "publisher": "…", "description": "…", "spatial_scope": "…", "data_formats": ["csv"], "formats": "csv", "access_kind": "…", "authentication_required": false, "rights": "…", "disposition": "admitted", "filters_match": true, "provider_id": "…", "provider_record_id": "…", "provider_record_url": "https://…", "entry_id": "…", "entry_coordinate": "…", "entry_digest": "…" } }, "next_step": "Each entry is discovery evidence, not a source: …"}Values are illustrative. formats is the one member this client derives. It is "unknown" when the catalogue holds no format for the entry, and "none declared" when the entry declares an empty list. data_formats beside it is the contract’s own value, untouched.
status is catalog_searched, catalog_no_admitted_source, catalog_search_incomplete or catalog_unavailable. On catalog_search_incomplete, incomplete_code says why the search stopped early. catalog_unavailable is not an error. That deployment has no catalogue to search, the payload includes refusal: {"code": "THIN_CATALOG_UNAVAILABLE", "detail": "…"} with every other block null, and the command exits 0.
Errors: THIN_RESPONSE_INVALID when Studio answers without the question, generation or work blocks that make a ranking citable, or with an unreadable entry. THIN_REQUEST_INVALID for a --limit outside 1 to 25, or a second --format.
mr-data catalog search "hourly electricity demand by US balancing authority" --format csv --limit 5 --jsonconnections
Section titled “connections”mr-data connections [--dataset DATASET] [--json]mr-data connections lists the saved connections this workspace holds, so a recipe can name one. It returns the connection identifiers only, no credential, and it changes no grant.
A recipe references a connection as source.connection, using connection_id and version_digest. The dataset needs an explicit grant, so set dataset.id in the recipe.
| Flag | Argument | Required | Meaning | Default |
|---|---|---|---|---|
--dataset |
DATASET |
no | Only show connections granted to this dataset ID. | every connection this workspace holds |
--json |
none | no | One JSON object instead of human lines. | off |
{ "schema_version": "mostlyright-thin-connections.v1", "connections": [ { "connection_id": "0f5b8c27-3a19-4d64-9e73-2c8d1a6f0b45", "name": "kalshi-prod", "display_name": "Kalshi (production)", "status": "active", "version_digest": "9f3c…", "origin": "https://api.elections.kalshi.com", "source_parameters": {} } ], "truncated": false, "next_step": "Use connection_id and version_digest in source.connection. The dataset must have an explicit grant; set dataset.id in the recipe."}Values are illustrative. A row leaves out a member the record does not carry rather than writing null.
Errors: THIN_RESPONSE_INVALID when Studio returns no connection list. THIN_AUTHORIZATION_DENIED, THIN_NOT_FOUND.
mr-data connections --dataset "$DATASET_ID" --json | jq '.connections[] | {connection_id, version_digest}'mr-data keys set NAME (--value-file FILE | --part NAME=VALUE …) [--json]mr-data keys list [--json]mr-data keys rm NAME [--json]mr-data keys stores a source credential by name so a recipe can reference it as {"secret_name": "…"}. It reads the value from a file or from standard input, never from the command line. Argv shows up in ps, in shell history and in CI logs.
The recipe carries the name. The worker redeems a one-time handle for the value when it fetches the source. Values never appear in recipes, receipts, events or logs.
| Flag | Argument | Required | Meaning | Default |
|---|---|---|---|---|
set|list|rm |
word | yes | set stores a credential under NAME, list names the ones this workspace holds, rm withdraws one. |
none |
NAME |
name | set, rm |
Lowercase letters, digits, dashes and underscores, starting with a letter, at most 63 characters. | none |
--value-file |
FILE |
with set |
The file holding the value. - reads standard input. One trailing newline is removed, because a credential stored with the one echo added fails at the source and nothing says why. At most 65536 bytes. |
none |
--part |
NAME=VALUE |
with set |
One named part of a shaped credential, which is what a stream venue authenticates with. NAME=@PATH reads that part off disk, which is how a signing key arrives. Repeatable, up to 4 parts. Cannot be combined with --value-file. |
none |
--json |
none | no | One JSON object instead of human lines. | off |
The parts named decide the shape, and the four sets are disjoint:
| Shape | Parts |
|---|---|
token |
token |
key_pair |
key_id, secret |
key_pair_passphrase |
key_id, secret, passphrase |
key_id_pem |
key_id, private_key_pem |
{ "schema_version": "mostlyright-thin-client-v4-secret.v1", "status": "secret_stored", "lane": "hosted", "secret": { "name": "kalshi-api", "status": "active", "created_at": "2026-09-12T09:00:00Z", "rotated_at": null, "rotation_generation": 1 }, "rotated": false, "trailing_newline_removed": true, "parts": [], "recipe_reference": { "secret_name": "kalshi-api" }, "note": "The value is stored and was not printed. …"}Values are illustrative. parts holds the part names on a shaped credential and never a value. rotated says this was a rotation rather than a first enrolment.
{ "schema_version": "mostlyright-thin-client-v4-secrets.v1", "status": "secrets_listed", "lane": "hosted", "count": 2, "secrets": { "credential 1": { "name": "kalshi-api", "status": "active", "created_at": "2026-08-…", "rotated_at": null } }}{ "schema_version": "mostlyright-thin-client-v4-secret-withdrawn.v1", "status": "secret_withdrawn", "lane": "hosted", "secret": { "name": "kalshi-api", "status": "revoked", "created_at": "…", "rotated_at": "…", "rotation_generation": 2 }, "note": "Nothing resolves kalshi-api any more: no future run redeems a handle for it and no recipe may reference it. Runs that already fetched with it are unchanged."}Errors:
| Code | When |
|---|---|
THIN_REQUEST_INVALID |
A name outside the grammar. set with neither --value-file nor --part, or with both. A --part that is not NAME=VALUE. More than four parts. A set of part names matching none of the four shapes. --part NAME=@PATH naming a file that could not be opened. |
THIN_SECRET_MATERIAL_INVALID |
The value, or one part, runs over 65536 bytes. The refusal names a size and never the value. |
THIN_STUDIO_* |
The vault’s own refusals, carried through under their own codes. |
printf '%s' "$KALSHI_TOKEN" | mr-data keys set kalshi-api --value-file - --jsonmr-data keys set kalshi-ws \ --part key_id=1d2c3b4a \ --part private_key_pem=@./kalshi.pem \ --jsonSee also
Section titled “See also”- CLI reference: conventions, exit codes, environment variables
- Runs: starting a run against what
reciperegistered - Streams: the venues a shaped credential authenticates against