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=. Keep the app and task in
task.py; a submitted file contains one typed job() and imports
them. simulo run train.py [--flags] maps flags onto that job’s
signature:
from typing import Any
import simulo
from task import CartpoleTask, app
@app.job(type="train", system=simulo.SystemType.TIER_1)def train(num_envs: int = 16, max_iterations: int = 2) -> dict[str, Any]: env = simulo.LearningEnv(task=CartpoleTask(), num_envs=num_envs, device="cuda") trainer = simulo.RLTrainer(env=env, algorithm="PPO", device="cuda", seed=42) stats = trainer.train(max_iterations=max_iterations) trainer.close() env.close() return {"num_envs": num_envs, **stats}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 example. task.py declares the app and the task, and train.py holds the file’s one typed job:
app = simulo.App("hello")
# train.pyfrom task import CartpoleTask, app
@app.job(type="train", system=simulo.SystemType.TIER_1, timeout=10 * 60)def train(num_envs: int = 16, max_iterations: int = 2) -> dict[str, Any]: env = simulo.LearningEnv(task=CartpoleTask(), num_envs=num_envs, device="cuda") trainer = simulo.RLTrainer(env=env, algorithm="PPO", device="cuda", seed=42) stats = trainer.train(max_iterations=max_iterations) trainer.close() env.close() return {"num_envs": num_envs, **stats}simulo run train.py --num-envs 16 --max-iterations 2 submits it: each job parameter becomes a flag. See simulo.Volume for a job that also mounts a writable scratch folder.
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, *, type: Optional[Union[JobType, str]] = None, callbacks: Sequence[JobCallbackProtocol] = (), 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.
type= is required and accepts a simulo.JobType or its
wire string, for example @app.job(type="train").
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, recorded into the package manifest and honored at execution.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 AND scheduled against: the control plane filters a tiered worker’s claim to its own tier plus untiered work, so the tier decides which GPU model runs the job. It is not a priority setting.simulo systemsreports this package’s catalog, not which tiers your platform has deployed. Naming a tier it has no fleet for is refused when you submit, and the error names the tiers it does have.timeout— Requested runtime 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). At submission, the platform uses the smaller of this value and your organization’s runtime limit. When that deadline expires, the job is reportedtimed_outwith reasonorg_runtime_limit_exceeded. Separately, every GPU machine enforces its ownMAX_JOB_TIMEOUT_Sceiling (1 hour by default); a job killed by that ceiling is reportedfailedwith reasontimeout. When omitted (None, the default), your organization’s effective runtime limit applies.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).JobDeclarationError—produces=orconsumes=was passed. Outputs are not declared on the decorator. Callbacks declare their own 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 train.py --from policy_x:bestinstead. 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.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).
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 one training function and submit its file with
simulo run train.py.
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.