Ask the user for budgetUsd — never choose one for them — and call submitJob only after the user has approved the estimate and that budget. Run a containerized workload on a rented cloud machine sized to the spec — GPU, and as much disk as minDiskGb asks for, so the local machine's disk and bandwidth are irrelevant (the box downloads datasets itself). Enqueues and returns a durable executionId immediately (status "queued"); the daemon provisions, runs, harvests artifacts and tears down, all under the budgetUsd ceiling. Write every deliverable to /workspace/artifacts/ — only that directory is harvested and returned by getArtifact; a command that saves its model to the working directory or any other path finishes with nothing delivered. image must be one of the images we have verified launch on our machines — an image must ship an ssh client to be launched: pytorch/pytorch:2.4.1-cuda12.1-cudnn9-runtime, pytorch/pytorch:2.4.0-cuda12.1-cudnn9-runtime, tensorflow/tensorflow:2.15.0. Any other image is refused with code invalid_spec, action fix_spec and a supportedImages list, before anything is booked or charged. budgetUsd is what you are willing to spend on the job, and you are never billed more than that. Call estimateExecution first if you have not priced this spec — it also tells you whether this account can cover the budget, and whether it holds free compute credit still to claim. A refusal here carries a machine-readable code (unauthorized, invalid_spec, budget_too_low, insufficient_balance, no_compute, unavailable, refused) and one action (authenticate, fix_spec, raise_budget, add_funds, claim_credit, relax_spec_or_retry, retry_later, contact_support), so branch on those rather than on the sentence; a refusal books no machine and charges nothing. insufficient_balance with action claim_credit means free credit is waiting on the account — claim it at https://symbioza.dev/app and resubmit. OPTIONAL: if your workload can count its own work, write /workspace/artifacts/.symbiosa/metrics.json with {"unit": "configuration", "units_planned": 100, "units_completed": 100, "elapsed_seconds": 4821} — unit is free text naming what you counted, partial progress is valid evidence on a failed run, and the whole file is optional (omit it and throughput is simply recorded as UNKNOWN; do not invent a unit to fill it in). It is recorded as observed evidence about your workload and never affects what you are charged; a missing, malformed or incoherent file changes nothing about the run. You may disconnect and poll getStatus later. If an attempt fails and the job is moved to a replacement machine, the newest checkpoint streamed off the previous box (files named ckpt_step<N>.pt, N = digits) is pushed onto the new box under /workspace/.symbiosa/resume/ (outside the artifacts directory) and SYMBIOSA_RESUME is set to its absolute path there — your command must load that file to continue, or the attempt restarts from the beginning. The variable is unset on a first attempt, when no checkpoint was kept, when kept checkpoints span more than one directory (multi-arm layouts are not restored), when checkpoint.enabled is false, when the box lacks disk headroom, or when the push failed; restore is best-effort salvage and never fails a run. Work since the last streamed checkpoint is lost, and the push's hash check proves the transfer, not that the checkpoint itself is intact. Set pinHost: true for a benchmark or measurement workload whose numbers are only comparable on ONE machine: the job then FAILS instead of being moved to a replacement box, because a silent host swap would make the results meaningless; no retry, resize or reroute buys a second machine for a pinned job. It is a refusal, not a migration — nothing holds the original machine. Only when the user explicitly requires a particular model for their own benchmark, set gpu.gpuNameExact: true alongside gpu.gpuName to pin the CARD MODEL the same way: only that model is booked and every substitute is refused, including a faster one, because when the measured quantity is the hardware a better card is as wrong as a slower one. pinHost holds the box within one run; gpuNameExact holds the model across every run, so a measurement campaign spanning several jobs declares both. The same model is sold at different board powers (350-600 W under one name), so for an energy or efficiency measurement also declare gpu.minPowerLimitW (watts): hosts below it are refused, and a host publishing no power figure is refused rather than assumed un-throttled. Declare gpu.minCudaVersion (e.g. 13.0) when your image pins a torch/flash-attn build against a CUDA version: only hosts whose driver advertises at least that CUDA are booked, and a host that publishes no CUDA ceiling is refused rather than booked. Declare datasetGb as the total data the box will ingest, including data it downloads itself (not only what datasets[] stages), so a host too slow to pull it in time is filtered before booking. Idempotent only with clientRequestId: the same key with the same spec returns the same job, marked duplicate: true, instead of booking and charging for a second one; the same key with a different spec is refused with invalid_spec — without a key, every call starts a new job. Pass the specDigest estimateExecution returned and a spec or budget changed since that estimate is refused with invalid_spec, so what runs is what you were quoted.
submitJob