simulo.App
The user-facing job container — declare it once, at module scope.
An app names your project and (optionally) mounts assets/volumes into
every job. Its jobs execute in a Simulo-curated runtime — the platform’s
default runtime unless the app picks a
different Simulo runtime via runtime=. Decorate functions with
job() to make them submittable — simulo run app.py [--flags]
submits the sole job with the flags mapped onto its own signature (--job NAME chooses among several). Optionally decorate one function with
entrypoint() to orchestrate what simulo run app.py does instead:
import simulo
app = simulo.App("cartpole")
@app.job(system=simulo.SystemType.TIER_1)def train(iterations: int = 1000): ...
@app.entrypointdef main(iterations: int = 1000): train.spawn(iterations=iterations)Args:
name— Non-empty app name, recorded in the package manifest.runtime— Optionally pick a different Simulo runtime for this app’s jobs (simulo.Runtime.from_registry("simulo/gpu-rl:2026.06")). Omitted means the platform default.mounts— Optional mapping of mount name tosimulo.Asset/simulo.Volume, shared by every job of this app.
Raises:
ValueError— ifnameis empty or whitespace-only.
Real usage, from the shipped hello app — the half the docstring’s own example above doesn’t show: a single job with no @app.entrypoint, invoked straight from the CLI:
app = simulo.App("hello")
@app.job(timeout=5 * 60)def hello(name: str = "world", repeat: int = 3) -> dict[str, Any]: for index in range(repeat): print(f"[hello] {index + 1}/{repeat}: hello, {name}!", flush=True) time.sleep(0.3) return {"greeting": f"hello, {name}", "repeat": repeat}simulo run app.py --name robot --repeat 5 submits it. See simulo.Volume for a job that also mounts durable storage.
simulo.App( name: str, *, runtime: Optional[object] = None, mounts: Optional[Mapping[str, Mount]] = None,)@app.job
Section titled “@app.job”App.job( fn: Optional[F] = None, *, callbacks: Sequence[JobCallbackProtocol] = (), resume: ResumePolicy = ResumePolicy.AUTO, system: Optional[SystemType] = None, timeout: Optional[int] = None, retries: int = 0, **extra: Any,) -> Union[JobFunction, Callable[[F], JobFunction]]Register a function as a job of this app. Records metadata; never runs the body.
Usable bare (@app.job) or with options
(@app.job(system=simulo.SystemType.TIER_1)).
Returns a JobFunction wrapping the original function — calling
it still runs the body in-process; .spawn() / .submit() submit it
to the platform instead.
Args:
fn— The function being decorated (bare-decorator form only — never pass it alongside keyword options).callbacks— Lifecycle callbacks fromsimulo.callbacks(e.g.ResumableCheckpoint), recorded into the package manifest and honored at execution.resume— How the job resumes from a discovered checkpoint (ResumePolicy, defaultAUTO).system— The GPU system tier to request, as asimulo.SystemTypemember (e.g.simulo.SystemType.TIER_1), recorded in the job’s resource spec asresources["system"](the tier’s wire value, e.g."tier1"). Validated HERE, at decoration time, against the published catalog: a value that is not aSystemTypemember (a raw string such as"tier1"included) raisesTypeError, and a tier the platform cannot provision yet raisesValueError.simulo systemslists every tier and which are available today. The selection is recorded on the job; it does not yet choose hardware.timeout— Wall-clock limit for one managed execution, in seconds (a positive integer; validated HERE, at decoration time — a zero, negative, or non-integer value raisesValueErrorimmediately rather than failing later at run time on the worker). Enforced by the platform worker: a job still running at the limit is killed and reportedfailedwith reasontimeout. When omitted (None, the default), the platform ceiling applies (1 hour by default); a declared value larger than the ceiling is clamped down to it. The ceiling is an operator knob on the worker (MAX_JOB_TIMEOUT_S=<seconds>) for long unattended training.retries— Automatic re-attempts after a failure (default 0). All attempts share onetimeoutbudget.**extra— Additional resource requests, recorded verbatim in the job’s resource spec. Three names are RESERVED and rejected —produces,consumesandgpu(see Raises below); everything else passes through to the resource spec unchanged.
Raises:
ValueError—timeoutis notNoneand is not a positive integer.TrueandFalseare not accepted as timeouts. Also raised whensystemnames a tier the platform cannot provision yet (SYSTEM_SPECS[system].availableis false).TypeError—produces=orconsumes=was passed. Outputs are not declared on the decorator. Callbacks such asResumableCheckpointalready declare their supported outputs. Return small JSON values from the job, or use a named volume for files another job needs.consumes=is reserved; continue from a checkpoint withsimulo run --from <job>[:latest|:best]instead. Also raised forgpu=(the free-text GPU request this parameter replaced; it would otherwise fall through**extrainto the resource spec unvalidated) and for asystemvalue that is not asimulo.SystemTypemember.
@app.entrypoint
Section titled “@app.entrypoint”App.entrypoint(fn: F) -> FRegister an entrypoint; redirect to the CLI if the file is python-ed.
Returns fn unchanged. simulo run app.py [--flags] — the canonical
(and only) submit command — imports the file as a NON-__main__ module
and invokes the registered entrypoint explicitly (mapping --flag value
onto the entrypoint’s parameters). An entrypoint is OPTIONAL: without
one, simulo run maps the flags onto the job’s own signature
(--job NAME chooses among several); register one only to
orchestrate multiple submits yourself.
When the decorator is applied from a directly-run python app.py —
detected by frame-walking to the first frame outside this module and
checking that its __name__ == "__main__" with a real __file__ —
nothing is submitted: the exact equivalent simulo run command (rebuilt
verbatim from sys.argv) is printed to stderr and the process exits 2.
The SystemExit fires at DECORATION time, mid-import, so a file ending
in if __name__ == "__main__": main() never reaches main() — an
at-exit-only guard would let main() run and, with saved credentials,
actually submit a cloud job. Gating on the calling frame rather than
fn.__module__ means the redirect fires even when the entrypoint
function is imported from another module. TYPED Jupyter/REPL cells
(__name__ == "__main__" but no __file__) never trigger it;
hosts that run the file under the __main__ name with a real
__file__ — runpy.run_path, IPython’s %run — DO see the
redirect, and that is safe by mechanism: this path raises
SystemExit (which such hosts catch and display) rather than
hard-exiting the host process.
App.name: strproperty
The app’s name, as passed at construction.
runtime
Section titled “runtime”App.runtime: Anyproperty
The Simulo runtime this app’s jobs execute in (the default unless picked).
mounts
Section titled “mounts”App.mounts: Mapping[str, Mount]property
The assets/volumes mounted into every job of this app, by mount name (a copy).
App.jobs: Mapping[str, JobSpec]property
The registered job specs, by job name (a copy).
entrypoint_names
Section titled “entrypoint_names”App.entrypoint_names: tuple[str, ...]property
Names of the functions registered with @app.entrypoint.
JobFunction
Section titled “JobFunction”What @app.job returns: the original function, wrapped for submission.
Calling it runs the body in-process, exactly as if it were undecorated (that
is also how the platform invokes it on a worker). spawn() and
submit() package and submit it instead. You never construct one
yourself — decorate a function and use it from your @app.entrypoint.
class JobFunctionspawn()
Section titled “spawn()”JobFunction.spawn(**kwargs: Any) -> JobHandleSubmit this job to the Simulo cloud (once logged in), or write it locally.
Submitting writes the manifest + source bundle (memoised once per app),
recording this job’s name and kwargs into the manifest so the job can
be run later with fn(**kwargs).
Cloud mode — credentials exist (simulo login), or
SIMULO_API_URL/SIMULO_ENV names a target (and SIMULO_SUBMIT is
not local): the package is tarred, uploaded, and a job is created on
the Simulo cloud. The returned JobHandle carries the
SERVER-assigned job_id and .get() streams the real result.
Local-disk mode (the default with no cloud signal present): NEVER
trains and never runs the job — the handle exposes only
.package_path / .job_id (a locally-derived id), and .get()
raises, pointing you at simulo login to submit to the cloud instead.
Positional arguments are rejected: a job body receives only the keyword arguments recorded in the manifest.
submit()
Section titled “submit()”JobFunction.submit(*args: object, **kwargs: object) -> JobIdSubmit this job (keyword args only): write the package, return its id.
Like spawn() but returns just the JobId (JobFunctionProtocol).
It writes the package and never executes; nothing is dispatched.