Run protocol v0.2¶
This page tells how a run moves from the API to a sandbox and back. It gives:
- the state machine of the run
- the bootstrap object and the environment that an execution gets
- the event stream that the runner writes
- the output manifest, the checks on it at finalisation, and the signed bundle manifest
- the storage layout
- the ports and the environment variables of a local installation.
Each shape on this page is a Pydantic model or a constant in mapi_spec.run,
mapi_spec.storage or mapi_spec.localdev. The API server, the executors, the runner and
the tests all import them from these modules.
sequenceDiagram
participant API
participant D as Dispatcher
participant S as Object store
participant X as Executor
participant R as Supervisor (runner)
participant W as Worker (plugin code)
API->>API: CREATED → QUEUED (static validation, quota)
D->>D: lease → DISPATCHING, intent row (run, attempt, nonce)
D->>S: PUT bootstrap/{nonce} (RunSpec: signed URLs, HMAC key)
D->>X: submit(env: MAPI_RUN_ID, MAPI_NONCE)
X->>R: start execution → PROVISIONING
R->>S: GET + DELETE bootstrap/{nonce}
R->>W: spawn with scrubbed env (socketpair)
R-->>API: stdout {"mapi_event": …} (HMAC per line) → RUNNING
W->>R: progress / results
R->>S: signed POST/PUT outputs + manifest.json
R-->>API: "complete" event, exit 0
API->>S: stat + list (metadata only), server-side copy → bundles
API->>API: FINALIZING → SUCCEEDED, signed BundleManifest
Run states¶
RunState values are in lower case on the wire. This page writes them in upper case.
| State | Meaning |
|---|---|
CREATED |
The API accepted the run. The static validation and the quota check are not complete yet. |
REJECTED |
Terminal. This state is for submissions that the static validation or the quota refuses. The current server does not make such runs. Refer to the note below the transition table. |
QUEUED |
The run waits for the dispatcher. |
DISPATCHING |
The dispatcher has the lease and wrote the intent row. The call to the executor is in progress. |
PROVISIONING |
The executor accepted the execution. No event came yet. |
RUNNING |
Minimum one verified event came. |
STALLED |
No event came for K × the progress interval. This is a warning, not a failure. The status API of the executor is the source of truth about the execution. |
FINALIZING |
The execution is complete. The control plane does a check of the outputs and copies them. |
SUCCEEDED |
Terminal. A verified bundle exists. |
PARTIAL |
Terminal, but you can resume the run. A verified partial bundle exists. The runner wrote it from checkpoints after a SIGTERM or at the deadline. |
FAILED |
Terminal, with one of these failure classes: user, resource_exhausted, infra, integrity, timeout or sandbox |
CANCELLING |
The user asked to cancel the run. The run waits for the executor to confirm. |
CANCELLED |
Terminal |
Transitions¶
mapi_spec.run.transition(state, trigger, actor=...) is a pure function. It returns the rule
that matches, or it raises IllegalTransition. IllegalTransition maps to 409 CONFLICT.
Callers save rule.target with a compare-and-set on the old state. If rule.new_attempt is
set, callers also increase attempt and make a new nonce. A test compares the table below with
the code for all 13 × 19 pairs of state and trigger.
| From | Trigger | To | Owner | Failure class | New attempt |
|---|---|---|---|---|---|
CREATED |
accept |
QUEUED |
api | ||
CREATED |
reject |
REJECTED |
api | ||
QUEUED |
lease |
DISPATCHING |
dispatcher | ||
DISPATCHING |
submitted |
PROVISIONING |
dispatcher, reconciler (orphan adoption) | ||
DISPATCHING |
infra_retry |
QUEUED |
dispatcher, reconciler | yes | |
DISPATCHING |
fail_infra |
FAILED |
dispatcher, reconciler | infra | |
PROVISIONING, RUNNING, STALLED |
event |
RUNNING |
events | ||
PROVISIONING, RUNNING, STALLED |
complete |
FINALIZING |
reconciler | ||
PROVISIONING, RUNNING, STALLED |
infra_retry |
QUEUED |
reconciler | yes | |
PROVISIONING, RUNNING, STALLED |
fail_user / fail_resource_exhausted / fail_infra / fail_timeout / fail_sandbox |
FAILED |
reconciler | user / resource_exhausted / infra / timeout / sandbox | |
RUNNING |
silence |
STALLED |
reconciler | ||
STALLED |
silence |
STALLED (no-op) |
reconciler | ||
FINALIZING |
event |
FINALIZING (no-op) |
events | ||
FINALIZING |
verified |
SUCCEEDED |
finalizer | ||
FINALIZING |
verified_partial |
PARTIAL |
finalizer | ||
FINALIZING |
fail_integrity |
FAILED |
finalizer | integrity | |
FINALIZING |
fail_infra |
FAILED |
finalizer | infra | |
every non-terminal state except CANCELLING |
cancel |
CANCELLING |
api | ||
CANCELLING |
cancel / event |
CANCELLING (no-op) |
api / events | ||
CANCELLING |
cancel_confirmed |
CANCELLED |
reconciler | ||
CANCELLING |
complete |
FINALIZING |
reconciler | ||
PARTIAL |
resume |
QUEUED |
api | yes |
A submission that is not valid makes no run. POST /v1/runs and the model :score
action do the static validation before a run row exists. If the validation finds an error,
the server refuses the request with a 4xx problem. The code of the problem is the code of the
first error. The errors list gives each issue with its JSON pointer. The server keeps
nothing, thus there is no REJECTED run and no run history for the request.
CREATED → reject → REJECTED stays in the table for a later server that records refused
submissions.
All components obey these rules:
- Events only move a run to
RUNNING. All other transitions during the execution come from the reconciler. The reconciler is level-triggered and reads the status of the executor. completeoccurs when the executor reports exit code0or16and thecompleteevent came. Then the finalizer readsOutputManifest.statusand startsverifiedorverified_partial.infra_retryoccurs only after the control plane confirms that the old execution stopped. The cost limits the automatic retries (MAPI_AUTO_RETRY_COST_FACTORtimes the estimate). A count also limits them (MAPI_MAX_INFRA_RETRIES). If a run used more than 30 minutes, there is no automatic retry without the consent of the user.- Late events. The events service ignores verified events for runs that are already
terminal. All terminal states reject
event. - A cancel of a
FINALIZINGrun has no effect at the API. The table permits the transition, butPOST /v1/runs/{id}:cancelreturns the run with no change. The execution is already complete. The finalizer can possibly have a version number already. It can also have copied write-once bundle objects.
Bootstrap¶
The executor does not send credentials, URLs or keys through argv or the environment. The only
values for each execution are MAPI_RUN_ID and MAPI_NONCE.
- The dispatcher writes an intent row
(run, attempt, nonce). The nonce is 256 random bits, written as 64 lowercase hex characters (new_nonce()). - The dispatcher writes a
RunSpectogs://mapi-{env}-bootstrap/{nonce}(mapi_spec.run.RunSpec, schemarunspec.json). - The dispatcher calls
Executor.submit(RunSubmit, nonce). For all out-of-process executors,RunSubmit.payloadstays empty. The executor sees only the run id, the attempt, the kind, the resource class, the timeout and the labels. All secret values go in theRunSpec. - The supervisor resolves
MAPI_BOOTSTRAP_URL, GETs the object, and deletes it immediately. Then it validates the object as aRunSpec. It starts the worker withworker_environ(), an allow-listed environment that has noMAPI_*variable. If the object is missing or not valid, the supervisor exits with code2 CONFIG_ERROR.
RunSpec field |
Meaning |
|---|---|
schema_version |
mapi.runspec/v1 |
run_id, attempt, nonce, kind |
The identity of the run. Parent kinds (sweep, backtest, sbc, campaign) never execute. |
tenant_id, project_id |
Always present (default in M1) |
spec, spec_hash, catalog_digest |
The resolved spec JSON (with pinned use: ranges), its canonical hash, and the catalog snapshot that the server used to resolve it |
inputs |
name → SignedGet{url, expires_at, generation, size, crc32c, md5_hash?}. The well-known names are dataset and bundle. Each URL is pinned to a generation and supports HTTP Range. The runner does a check of the size and the CRC32C after the download. If they do not agree, it exits with 13 INTEGRITY_ERROR. |
outputs |
name → SignedPut \| PostPolicy. manifest is a necessary write-once signed PUT for {prefix}manifest.json. artifacts is a create-only V4 POST policy on the attempt prefix with a content_length_range. It cannot replace an object that exists. Thus it cannot overwrite the manifest or replace an output after the runner wrote it. checkpoints is a POST policy on checkpoint_prefix. It is present only for runs that can write checkpoints. media (v0.14) is a create-only POST policy on media_prefix for the images and tables that the run logs. |
resume |
The checkpoint objects of the newest earlier attempt that wrote checkpoints, as SignedGets. The server ignores an attempt that stopped before its first checkpoint. |
checkpoint_prefix |
{attempt prefix}checkpoints/, or null |
media_prefix |
v0.14: {attempt prefix}media/, or absent. When outputs has media, its prefix must be media_prefix. |
hmac_key |
The event HMAC key of the run (new_hmac_key()), in base64. It has a minimum of 32 random bytes. The logs never show it: RunSpec.redacted() removes it and all URLs. |
limits |
RunLimits: log_line_bytes (4096), log_total_bytes (20 MiB), max_event_bytes (64 KiB), progress_interval_s (2 s), max_output_bytes and scratch_bytes (from the resource class), max_output_objects (10 000), and from v0.11 tracking (a TrackingLimits, see Tracking budgets) |
resource_class, deadline, created_at, labels |
The class (S/M/L/XL, and from v0.15 GPU), a hard UTC deadline, and metadata |
input_layouts |
v0.8: name → layout for dataset inputs that are not a plain dataset: tree (a data.tree.zip) or, from v0.15, collection (an image collection, see Image collections). An input that is not in the map is a dataset. |
Environment¶
| Variable | Set by | Value |
|---|---|---|
MAPI_RUN_ID |
executor, for each execution | The run id |
MAPI_NONCE |
executor, for each execution | 64 hex characters |
MAPI_BOOTSTRAP_URL |
executor, static for each Job or backend | A template with exactly one {nonce}. Cloud: gs://mapi-prod-bootstrap/{nonce}. The execution service account reads it. This account can get and delete, but it cannot list. Local: http://127.0.0.1:4443/storage/v1/b/mapi-local-bootstrap/o/{nonce}. |
MAPI_EXEC_TOKEN |
local executor only | The bearer token of the exec principal of the GCS shim. This principal can get and delete in the bootstrap bucket. Never set in the cloud. |
The worker gets only the variables of WORKER_ENV_ALLOWLIST. From v0.15 the list also has
JAX_PLATFORMS, XLA_PYTHON_CLIENT_PREALLOCATE, XLA_PYTHON_CLIENT_MEM_FRACTION and
CUDA_VISIBLE_DEVICES, for the networks of mapi-jax.
RunnerEnv.from_environ() parses these variables and gives the name of each missing variable.
RunnerEnv.resolved_bootstrap_url() puts the nonce into the template.
Events¶
The worker cannot write to the real stdout. It sends reports to the supervisor through a socketpair. The supervisor captures the stdout and the stderr of the worker. It removes ANSI and control sequences. Then it writes one JSON object on each line to its own stdout:
{"mapi_event":{"attempt":1,"hmac":"5f0c…","payload":{"chains":[],"fraction":0.4,"phase":"sample"},"run_id":"run_01jaf3q2x8m9v7k5h4g3f2e1d0","seq":12,"ts":"2026-10-15T09:31:00.123456Z","type":"progress"}}
Cloud Logging keeps each line as the jsonPayload of a log entry. The sink filter is
jsonPayload.mapi_event:*. The platform compares the identity of the run with the label of
the execution. The local executor reads the same lines from the stdout pipe of the supervisor.
encode_event_line() and decode_event_line() implement the wire format:
- A line that is not JSON, or JSON without
mapi_event, is not an event. The platform ignores it. - A
mapi_eventobject that is not valid, or a line of more than 64 KiB, raisesInvalidEvent. This is aSANDBOX_VIOLATION.
Each event has these fields:
| Field | Rule |
|---|---|
run_id, attempt |
Must agree with the run and its current attempt |
seq |
Starts at 0 for each attempt and increases by 1 for each line. The receiver removes duplicates on (run_id, attempt, seq). Gaps are permitted, because the logs can lose entries. |
ts |
Has a timezone and is normalised to UTC |
type |
state, progress, log, diagnostic, warning, checkpoint, metric, metrics, params, tags, media (v0.14), complete or error |
payload |
Validated against the payload model for type. Refer to the table below. |
hmac |
Lower-case hex HMAC-SHA256 with the hmac_key of the run |
The data that the HMAC signs:
Canonical JSON (canonical_bytes()) has these rules:
- The model is dumped in JSON mode with the defaults.
- The keys are sorted.
- There is no whitespace.
- The text is UTF-8 with no ASCII escapes.
- Floats with integral values are written as integers.
- NaN and Infinity are not permitted.
The verification uses a constant-time comparison (verify_event()). If an event fails the
verification, the platform ignores it and counts a sandbox violation for that run. Such an
event never changes the state of the run. Output manifests use the context tag
mapi.output-manifest.v1. Bundle manifests use mapi.bundle-manifest.v1. Thus a signature
from one domain is never valid in a different domain.
type |
Payload model | Notes |
|---|---|---|
state |
StatePayload{phase, message?} |
The runner phases: bootstrap, load_inputs, validate, preprocess, fit, sample, score, evaluate, derive, plots, upload, done |
progress |
ProgressPayload{phase, fraction?, message?, eta_seconds?, chains[]} |
Maximum one in each limits.progress_interval_s. The platform writes it into one run_progress row (upsert). From v0.11, samplers fill chains (ChainProgress{chain, draws_done, draws_total, tuning, divergences, step_size?, num_steps?}). |
log |
LogPayload{stream, line, truncated, level?, fields?} |
One line, maximum log_line_bytes. After log_total_bytes, the run fails with SANDBOX_VIOLATION (log flood). The streams are stdout, stderr, system and, from v0.11, user. Only user lines have level and fields. |
diagnostic |
Diagnostic (mapi_spec) |
Goes into the scorecard |
warning |
WarningPayload{code, message, untrusted_text?} |
The run continues. v0.11: TRACKING_BUDGET (Tracking budgets). v0.14: the media warnings (Media). v0.15: GPU_UNAVAILABLE when a run of the GPU class sees no NVIDIA device (the run continues on the CPU), VIEW_SAMPLE_FAILED when a collection fit cannot decode the sample for its views, and CHECKPOINT_SKIPPED (Checkpoints of networks). Troubleshooting tells what to do. |
checkpoint |
CheckpointPayload{name, size, crc32c, segment, draws_done?} |
name is relative to checkpoint_prefix |
metric |
MetricPayload{name, value, step?, labels} |
One scalar, for example a holdout metric at the end of a fit. From v0.11, the platform also keeps it as a point of the metric series name. |
metrics |
MetricBatchPayload{t0, series[]} |
v0.11. Points of metric series. See Tracking events. |
params |
ParamsPayload{params[]} |
v0.11. Run params. The first value of a key is kept. |
tags |
TagsPayload{tags[]} |
v0.11. Run tags. The last value of a key is kept. |
media |
MediaPayload{key, kind, content_type, name, sha256, size, crc32c, step?, labels, width?, height?, caption?} |
v0.14. An image or a table that the supervisor uploaded below media_prefix. See Media. |
complete |
CompletePayload{status, manifest_name, manifest_crc32c, objects, total_bytes} |
The last event of a successful attempt (complete) or of an attempt with checkpoints (partial) |
error |
ErrorPayload{code, failure_class, message, untrusted_text?} |
code must be equal to failure_class.error_code |
make_event(run_id, attempt, seq, payload, key=...) gets type from the payload model and
signs the event.
Tracking events v0.11¶
The events metrics, params and tags carry the tracking data of a run. The platform writes
them into the tracking tables (metric points, series summaries, params and tags). It does not
keep them in the event list. Thus, GET /v1/runs/{run_id}/events does not return them. The
event stream sends a tracking frame when they change (see
HTTP API).
A metrics payload groups its points by series. A series is one metric key and one set of
labels. The point lists are columns of equal length:
{"t0": "2026-10-15T09:31:00Z",
"series": [{"key": "step_size", "labels": {"chain": "0"},
"steps": [399, 400], "values": [0.32, 0.31], "ts_ms": [0, 250]}]}
| Field | Rule |
|---|---|
t0 |
A time with a timezone. The time of a point is t0 + ts_ms milliseconds. |
series |
1 to 64 MetricSeriesBatch items |
key |
A metric key: ^[a-z][a-z0-9_]{0,63}$. Use labels for qualifiers. |
labels |
A maximum of 8. Key: ^[a-z][a-z0-9_]{0,31}$. Value: ^[A-Za-z0-9_.\-]{1,63}$. |
steps |
An integer (0 or more) for each point, or null for a scalar point |
values |
Finite numbers only. NaN and infinity are not valid. |
ts_ms |
An integer (0 or more) for each point |
One metrics event has a maximum of 1000 points (MAX_POINTS_PER_EVENT). One params event
has a maximum of 200 items, and one tags event has a maximum of 100 items. A param value is
a boolean, an integer, a float or a text of a maximum of 6000 characters. A tag value is a
text of a maximum of 1000 characters. A tag key that starts with mapi. is not valid.
An event line must not be more than max_event_bytes. split_for_events(payload,
max_event_bytes) divides a payload into parts that each fit in one line. It divides by
series first, then by points or items, and it keeps the sequence. Each part keeps 2048 bytes
for the envelope of the event.
Worker messages¶
The worker sends tracking data to the supervisor through the socketpair, with these messages
(mapi_runner.channel):
| Message | Payload | Rule |
|---|---|---|
metrics |
MetricBatchPayload |
The worker collects points for each series and sends them each flush_interval_s (1 s), or when 1000 points wait |
params |
ParamsPayload |
Sent with the next flush |
tags |
TagsPayload |
Sent with the next flush |
user_log |
LogPayload with stream user |
Sent immediately. Another stream is a protocol error. |
The worker sends the last points before its result or error message. StartMessage
gives the worker the TrackingLimits of the run.
Tracking budgets¶
RunLimits.tracking (TrackingLimits) gives the budgets of each run. The worker, the
supervisor and the server use the same budgets.
| Field | Default | Meaning |
|---|---|---|
metric_points_per_series |
10 000 | Points in one series |
metric_points_total |
500 000 | Points in all the series of the run |
metric_series |
1000 | Different (key, labels) series |
params |
5000 | Params |
tags |
100 | Tags (also the maximum) |
log_lines |
10 000 | user log lines |
log_bytes |
2 MiB | Bytes of user log lines |
tracking_bytes |
64 MiB | Bytes of all tracking messages |
flush_interval_s |
1.0 | Seconds between two flushes of the worker (0.1 to 30) |
When a run logs more than a budget, the worker and the supervisor do not keep the points or
the items that are more than the budget. They count them. At the end of the run, the run gets
one warning with the code TRACKING_BUDGET. The warning gives the reasons and the series with
the most dropped points. The run does not fail.
tracking_bytes is a hard limit. If the worker sends more bytes of tracking messages than this
limit, the supervisor stops the run with SANDBOX_VIOLATION (tracking flood), like a log
flood.
From v0.14, TrackingLimits also has the budgets of the media of a run:
| Field | Default | Meaning |
|---|---|---|
media_items |
1000 | Different media items (key, labels and step) |
media_bytes |
256 MiB | Bytes of all media items of the run |
media_item_bytes |
8 MiB | Bytes of one media item (1024 to 8 MiB) |
Media v0.14¶
The media event tells the server that the supervisor uploaded an image or a table of the
run. Like the tracking events, the server does not keep it in the event list. It writes a row
of run_media, and the event stream sends a tracking frame with media: true.
A media item goes through these steps:
- The worker encodes the item (
ctx.log_image,log_figureorlog_table). It writes the bytes to{scratch}/media/<sha256>.<ext>(create-only) and sends amediamessage to the supervisor.StartMessage.mediaisfalsewhen the run has nomediapolicy. Then the worker does not write files. - The supervisor opens the file with
O_NOFOLLOW(a regular file only), with the size limitmedia_item_bytes. It reads the bytes and deletes the file. It calculates the SHA-256 and callscheck_media(kind, content_type, data). - The supervisor uploads the bytes through the
mediaPOST policy to{media_prefix}<sha256>.<ext>. An item with the same SHA-256 in the same attempt is not uploaded again. - The supervisor sends a
mediaevent with aMediaPayload. - The server downloads the object from the runs bucket (a maximum of 8 MiB). It calculates
the SHA-256 again, and does a check of the size, the format and the image size. It writes
the bytes create-only to the bundles bucket at
t/{tenant}/run-media/{run}/<sha256>.<ext>(run_media_store_key). Then it writes the row.
The sandbox never writes to a shared path. The runs bucket deletes run objects after 30 days. The copy in the bundles bucket stays until the tenant is deleted.
MediaPayload:
| Field | Rule |
|---|---|
key |
A metric key. Use labels for qualifiers. |
kind |
image or table |
content_type |
image/png or image/jpeg (kind image), application/json (kind table) |
name |
<sha256>.<ext> (.png, .jpg or .json): the object name below media_prefix. It must agree with sha256 and content_type. |
sha256 |
64 lowercase hex characters |
size, crc32c |
1 to 8 MiB. The CRC32C that the store calculated for the upload. |
step, labels |
As for a metric point |
width, height |
Necessary for an image (1 to 4096). Not valid for a table. |
caption |
A maximum of 500 characters. It is untrusted text. |
check_media (mapi_spec.media) accepts:
- A PNG or a JPEG image of a maximum of 8 MiB, with a maximum of 4096 pixels on each side.
sniff_imagereads the size from the PNGIHDRchunk, or from the first JPEG frame header in the first MiB. It does not decode the pixels. GIF, SVG, WebP and other data are not valid. - A table: UTF-8 JSON of a maximum of 1 MiB that validates as a
MediaTable(columns, androwsof equal length; a maximum of 10 000 rows, 64 columns and 200 000 cells; cells are numbers, booleans, text of a maximum of 1000 characters, ornull).
The worker messages and the warnings for media:
| Message or warning | Meaning |
|---|---|
media (worker message, MediaMessage) |
file (<sha256>.<ext>), key, kind, content_type, step, labels and caption. One message for each item. |
MEDIA_IGNORED (warning) |
The run has no media policy. The supervisor ignores the media of the run (one warning). |
MEDIA_BUDGET (warning) |
The run logged more than media_items or media_bytes. The supervisor counts the items of the attempt. The server counts the items of all attempts and the items logged through the API. Each one drops the extra items and writes one warning for the attempt. |
MEDIA_INVALID (warning) |
The file is not a regular file, is too large, or check_media refused it. The text of the worker is in untrusted_text. |
MEDIA_UPLOAD_FAILED (warning) |
The upload failed. The run continues. |
MEDIA_REJECTED (warning, server) |
The server checks failed: the object is missing, or its SHA-256, size or format does not agree with the event. The server writes no row. |
Image collections v0.15¶
A run that reads an image collection gets more than one input for the dataset. For the
dataset input dataset:
| Input | Object |
|---|---|
dataset |
manifest.parquet of the version |
dataset.info |
collection.json (CollectionInfo) |
dataset.pack.0000, dataset.pack.0001 … |
The packs, in the order of CollectionInfo.packs (pack_input(i)) |
schema |
schema.json, when present |
RunSpec.input_layouts["dataset"] is collection. The server sets it for each input whose
object name ends with /manifest.parquet. mapi_spec.collection has the names:
info_input(prefix), pack_input(i, prefix) and part_input(i).
A pack is a zip file. Each entry is named <sha256>.<ext>. Its address is
pack_digest(size, crc32c, md5): the SHA-256 of the canonical JSON of the store's own
size, CRC32C and MD5 of the object. This is the same basis as the address of a dataset
version. The runner calculates the digest of each signed pack URL again before it reads
the pack. A pack without an MD5 or with a different digest stops the run with
INTEGRITY_FAILURE. The runner reads the packs in ranges (HTTP Range), and it makes sure
that each image has the SHA-256 of its name.
The finalizer of a collection ingest:
- reads and validates
collection.jsonandschema.json; - for each pack with
source: upload, gets the stored size, CRC32C and MD5 of the pinned generation of itspart.<i>input, calculatespack_digestagain and compares it with the digest of the pack; - copies the part to
t/{tenant}/blobs/packs/{digest}.zip(create-only, the generation pinned); - for each pack with
source: base, makes sure that the base version has it.
The sandbox never writes to the shared pack path.
Checkpoints of networks¶
From v0.15, a model that trains in epochs saves checkpoints as files (FileCheckpointSink).
The names in checkpoint_prefix are:
| Name | Contents |
|---|---|
ckpt-<key>.weights.safetensors |
The weights of the network |
ckpt-<key>.opt.safetensors |
The state of the optimizer |
ckpt-<key>.meta.json |
The epoch, the step, the early-stopping state and the history |
<key> has 16 to 64 lowercase hex characters (the fit key). The checkpoint event has
segment = the epochs done and no draws_done. The samplers keep the names
ckpt-<key>.draws.zarr.zip and ckpt-<key>.meta.json. The supervisor uploads
.safetensors files with the content type application/octet-stream. A meta.json has a
maximum of 16 MiB. If it is larger, the worker does not save the checkpoint and sends the
warning CHECKPOINT_SKIPPED. A resume ignores a meta.json that is larger.
Exit codes¶
The supervisor exits with 0 only after it uploads the manifest and sends the complete
event. failure_class_for_exit() gives the failure class of the code. All other non-zero
codes are infra, for example 1 from a supervisor that crashed.
| Code | Name | Failure class |
|---|---|---|
| 0 | OK |
— (success) |
| 2 | CONFIG_ERROR |
infra: the environment is bad, or the bootstrap is missing or not valid |
| 10 | USER_ERROR |
user: plugin code or user code raised an exception, or made output that is not valid |
| 11 | RESOURCE_EXHAUSTED |
resource_exhausted: MemoryError, or the run used more than the scratch budget |
| 12 | INFRA_ERROR |
infra: the uploads or downloads of the supervisor failed after retries. (An INFRA_ERROR that the worker reports exits with 10.) |
| 13 | INTEGRITY_ERROR |
integrity: the size or the CRC32C of an input was not correct |
| 14 | TIMEOUT |
timeout: the run got to the deadline with no checkpoint to flush |
| 15 | SANDBOX_VIOLATION |
sandbox: log flood, output caps exceeded, or forged or oversized events |
| 16 | PARTIAL |
— (SIGTERM or deadline. The runner flushed a checkpoint and uploaded a partial manifest.) |
| 137 | KILLED |
resource_exhausted: SIGKILL, usually from the OOM killer. The platform offers "retry at class + 1" to the user. |
| 143 | TERMINATED |
timeout: the runner did not stop in the permitted time after SIGTERM |
When the user cancels a run, the run is already in CANCELLING. Thus the exit code does not
set the result, with one exception. A fit that writes checkpoints can get the SIGTERM. Then it
flushes its last checkpoint, uploads a partial manifest, sends a partial complete event
and exits with 16. The reconciler then starts complete from CANCELLING, and the
finalizer verifies the partial bundle. Thus the run ends in PARTIAL, and you can resume it.
All other exits end in CANCELLED.
Outputs and finalisation¶
The runner uploads all outputs below its attempt prefix
t/{tenant}/runs/{run}/a{attempt}/ in the runs bucket. It uses the artifacts POST policy.
It streams each object and calculates CRC32C and MD5 during the stream. Last, it PUTs
manifest.json, an OutputManifest that it signs with the HMAC key of the run:
{
"schema_version": "mapi.output-manifest/v1",
"tenant_id": "default", "project_id": "default",
"run_id": "run_01jaf3q2x8m9v7k5h4g3f2e1d0", "attempt": 1, "kind": "fit",
"status": "complete",
"prefix": "t/default/runs/run_01jaf3q2x8m9v7k5h4g3f2e1d0/a1/",
"spec_hash": "sha256:…", "catalog_digest": "sha256:…",
"inputs": {"dataset": {"generation": 3, "size": 1048576, "crc32c": "4waSgw=="}},
"objects": {
"bundle.zarr.zip": {"key": "t/default/runs/run_01jaf3q2x8m9v7k5h4g3f2e1d0/a1/bundle.zarr.zip",
"size": 2097152, "crc32c": "AAAAAA==", "md5": "1B2M2Y8AsgTpgAmY7PhCfg=="}
},
"runner_version": "0.2.0", "created_at": "2026-10-15T10:20:00Z", "hmac": "…"
}
The key of each object must be equal to prefix + name. Names must not have empty, . or
.. segments. The manifest must not list itself.
The hashes use the GCS conventions:
- CRC32C is the base64 encoding of the 4-byte big-endian value (
crc32c_to_base64()). - MD5 is the base64 encoding of the 16-byte digest.
The manifest is a claim. The control plane verifies it in the FINALIZING state. It does
not read the content of the objects:
- Metadata only. The control plane lists the attempt prefix and does a
statof the objects. This gives thesize,crc32c,md5Hashandgenerationthat the server calculated. -
Compare.
verify_output_objects(manifest, listing, expected_prefix=…)needs these conditions:- Each object in the manifest exists with the same size, CRC32C and MD5.
- No unexpected object exists below the prefix.
manifest.json,checkpoints/…and (v0.14)media/…are permitted.
Each difference is an
INTEGRITY_FAILUREissue. The run moves toFAILED(integrity), and the platform raises a security alert. 3. Validate sidecars. The control plane reads the JSON sidecars with size caps and validates them with Pydantic. The sidecars arediagnostics.json,plots.json,metrics.jsonandcoords.json,schema.jsonandsummary.jsonfor ingest, andcatalog.jsonfor describe. It also sanitizes the plots. 4. Server-side copy. The control plane copies each verified object into the bundles bucket belowt/{tenant}/models/{model}/{version}/. The copy pinssource_generationto the verified generation and usesif_generation_match=0. Thus bundle objects are write-once. The execution service account has no access to this bucket. 5. Sign. The platform writes aBundleManifestand signs it. The manifest binds the tenant, the run, the base image, the overlay hash, the spec hash, the data hash, the catalog digest and each destination generation. The signature is: - M1 (local):KMS-EC-P256-SHA256with a local P-256 key. The server keeps the key belowMAPI_DATA_DIR/keys/. This is an emulation of the KMS signer. Thus you can replace it with a real KMS key, and the signed bytes do not change. The server refuses bundles with theHS256signature when it makes URLs. - v0.3 (M2): a Cloud KMS asymmetric signature on the same bytes.The control plane makes URLs to the bundle only after
verify_bundle_manifest()is successful.
Job payloads and object names¶
mapi_spec.jobs is the contract between the dispatcher and the runner. The dispatcher writes
RunSpec.spec and gives the names of the inputs. The runner reads them and gives the names
of the outputs. The two sides import the module. Thus, if you change the name of an input or
change a payload, the type checks and the tests fail on the two sides.
| Kind | RunSpec.spec |
Inputs | Outputs that the control plane reads |
|---|---|---|---|
ingest |
IngestJob (a mapping or the MFF preset. dataset and source are only for information.) |
upload |
data.zarr.zip, schema.json (DataSchema), summary.json (DataSummary), profile.json, preview.json |
ingest of a collection (v0.15) |
IngestJob with collection (CollectionIngest) |
manifest, part.0000 …, and with a base version base.manifest, base.info, base.pack.0000 … |
manifest.parquet, collection.json (CollectionInfo), members.bin, schema.json, summary.json, profile.json, preview.json, columns.json (optional), ui/explore/…. No data.zarr.zip. |
describe |
DescribeJob (image_digest) |
none | catalog.json (Catalog) |
validate |
the resolved ModelSpec |
dataset, side.<name> |
validation.json |
fit |
the resolved ModelSpec (with pinned use: and dataset references) |
dataset, side.<name> |
bundle.zarr.zip, diagnostics.json, metrics.json (MetricsSidecar), plots.json, coords.json (ScoreMetadata.fit_coords), ui/… |
score |
the ScoreRequest |
bundle, bundle.manifest, dataset (the data of the request), scenario, side.<name>, history (v0.4: the fit dataset, for forecasts with future_covariates set to carry_last or seasonal_naive), inline.data / inline.scenario (v0.4: the inline XrJSON of an endpoint call, for use when dataset / scenario is not present) |
score.zarr.zip, result.json, summary.json (ScoreSummary, which the API puts in ScoreCallResult.summary), plots.json, ui/summary.arrow. Also ui/guided/forecast.json or ui/guided/counterfactual.json (v0.4). Also ui/fields.json (FieldsView) for the grid outputs of a predict, forecast or evaluate run. The default plots follow the shape of the outputs (see Runner), and grid outputs carry the attr grid |
evaluate / derive / render |
EvaluateJob / DeriveJob / RenderJob from the params of the run |
bundle, dataset (default: the fit dataset), side.<name>. plots for render. |
as the runner reference shows |
side_table_input(name) makes side.<name>. SCORE_INPUTS maps the data and scenario
fields of a ScoreRequest to their input names. A *.zarr.zip dataset can contain its raw
DataSchema as JSON in the root attribute DATASET_SCHEMA_ATTR (mapi_schema). Ingest
writes this attribute. If an upload has this attribute, the upload keeps its dim policies,
roles and units. The platform does not infer a schema for it.
inline.data and inline.scenario (mapi_spec.endpoints.INPUT_INLINE_DATA and
INPUT_INLINE_SCENARIO) contain the XrJsonDataset JSON of an endpoint score call. The API
keeps them in the quarantine bucket. The worker decodes them with the caps of the wire format:
strict JSON, no NaN or Infinity, and a maximum of 5 MB. The worker gives each dim
coordinate the type from the raw schema of the bundle:
- dates from ISO strings
- whole numbers for
integerandindexdims - text for categorical dims.
If the worker cannot read a value, the run fails with SCHEMA_INVALID.
UI views¶
Small JSON views for the browser go next to datasets and bundles. The control plane never
decodes them. The web app gets them through signed URLs. mapi_spec.views defines them, and
each view has a format tag:
| Object | format |
Model |
|---|---|---|
preview.json |
mapi.preview/v1 |
PreviewView: the dim columns, then the variable columns, the first rows, and total_rows |
profile.json |
mapi.profile/v1 |
ProfileView: the statistics for each variable, and a grid of missing values along the time-like dim (maximum 1,000 binned columns) |
ui/tree_index.json |
mapi.tree-index/v1 |
TreeIndexView: the dims, coordinates (with values for small coordinates), variables and scalar attributes of each DataTree node. It has no arrays. |
ui/summary.arrow, ui/grids/*.arrow |
Arrow IPC | Tidy tables (mapi_sdk.wire.to_arrow_table): one column for each dim, then one column for each variable. The schema metadata mapi.attrs holds the attrs as JSON. From v0.9, the schema metadata mapi.dims holds the names of the dim columns as a JSON list |
ui/fields.json |
mapi.model-explorer.fields/v1 |
FieldsView: the maps of the grid outputs of a score run, with the frames, bands and draws. It is the same view as the field section of the Model Explorer |
ui/preprocess.json |
mapi.preprocess/v1 |
PreprocessView: each pipeline step in sequence (id, resolved use, kind, stateful). For each step, it gives the learned JSON (maximum 16 KB, learned_truncated). It also gives the numeric state arrays with their labels, if they have a maximum of two dims and 10,000 cells. omitted gives the names of larger arrays. |
Storage layout¶
mapi_spec.storage makes each key on the server from validated ids (check_segment). User
input never becomes a path.
Bucket (Buckets(env)) |
Keys | Access |
|---|---|---|
mapi-{env}-quarantine |
t/{tenant}/uploads/{upload_id} |
Signed PUT from the browser. 7-day lifecycle. |
mapi-{env}-bootstrap |
{nonce} |
The execution identity can get and delete, but it cannot list. 1-day lifecycle. |
mapi-{env}-runs |
t/{tenant}/runs/{run}/a{n}/[tasks/{i}/], with checkpoints/ and (v0.14) media/ in it |
Written only through signed URLs |
mapi-{env}-bundles |
t/{tenant}/models/{model}/{version}/: bundle.zarr.zip, manifest.json, manifest.sig, diagnostics.json, plots.json, metrics.json, ui/…. From v0.14 also t/{tenant}/run-media/{run}/<sha256>.<ext> (the verified media of the runs). |
Control plane only. Write-once. |
mapi-{env}-data |
t/{tenant}/datasets/{sha256}/: data.zarr.zip, schema.json, summary.json, profile.json, preview.json. A collection version (v0.15) has manifest.parquet, collection.json and members.bin in place of data.zarr.zip. From v0.15 also t/{tenant}/blobs/packs/{digest}.zip (the packs of image collections, shared by the versions of the tenant) |
Content-addressed |
The control plane gets access to storage only through the async ObjectStore protocol:
- read and write:
put,get(with byte ranges and amax_bytescap),stat(metadata only),list,deleteandcopy - sign:
sign_get(pinned to a generation),sign_put(write-once by default) andpost_policy.
if_generation_match=0 means "only if the object does not exist yet". Failures raise
NotFound, PreconditionFailed or AccessDenied.
Local development¶
| Component | Address | Override |
|---|---|---|
| API (FastAPI) | http://127.0.0.1:8000 |
MAPI_API_URL |
| GCS shim (local Cloud Storage emulator) | http://127.0.0.1:4443 |
MAPI_GCS_ENDPOINT |
| Web app (Vite dev server) | http://127.0.0.1:5173 |
MAPI_WEB_ORIGIN |
| Plot sandbox | http://127.0.0.1:5174 |
MAPI_PLOTS_ORIGIN |
| Postgres 16 | Embedded pgserver, with its data in MAPI_DATA_DIR (default .local/) |
DATABASE_URL (any postgresql:// URL) |
The plot sandbox must have an origin that is different from the web app. The reason is
that plots render in a sandbox="allow-scripts" iframe with no same-origin access. LocalDev
refuses a configuration that puts the two on the same origin.
Other environment variables and helpers:
MAPI_ENV(defaultlocal) selects the bucket names, for examplemapi-local-runs.LocalDev.from_environ(os.environ)resolves the full configuration.LocalDev.bootstrap_url_template()gives theMAPI_BOOTSTRAP_URLvalue for the local executor.
You do not need a Docker daemon. Postgres comes from pgserver, and storage comes from the
GCS shim.