Skip to content

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.entrypoint
def 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 to simulo.Asset / simulo.Volume, shared by every job of this app.

Raises:

  • ValueError — if name is 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(
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 from simulo.callbacks (e.g. ResumableCheckpoint), recorded into the package manifest and honored at execution.
  • resume — How the job resumes from a discovered checkpoint (ResumePolicy, default AUTO).
  • system — The GPU system tier to request, as a simulo.SystemType member (e.g. simulo.SystemType.TIER_1), recorded in the job’s resource spec as resources["system"] (the tier’s wire value, e.g. "tier1"). Validated HERE, at decoration time, against the published catalog: a value that is not a SystemType member (a raw string such as "tier1" included) raises TypeError, and a tier the platform cannot provision yet raises ValueError. simulo systems lists 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 raises ValueError immediately 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 reported failed with reason timeout. 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 one timeout budget.
  • **extra — Additional resource requests, recorded verbatim in the job’s resource spec. Three names are RESERVED and rejected — produces, consumes and gpu (see Raises below); everything else passes through to the resource spec unchanged.

Raises:

  • ValueErrortimeout is not None and is not a positive integer. True and False are not accepted as timeouts. Also raised when system names a tier the platform cannot provision yet (SYSTEM_SPECS[system].available is false).
  • TypeErrorproduces= or consumes= was passed. Outputs are not declared on the decorator. Callbacks such as ResumableCheckpoint already 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 with simulo run --from <job>[:latest|:best] instead. Also raised for gpu= (the free-text GPU request this parameter replaced; it would otherwise fall through **extra into the resource spec unvalidated) and for a system value that is not a simulo.SystemType member.
App.entrypoint(fn: F) -> F

Register 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: str

property

The app’s name, as passed at construction.

App.runtime: Any

property

The Simulo runtime this app’s jobs execute in (the default unless picked).

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).

App.entrypoint_names: tuple[str, ...]

property

Names of the functions registered with @app.entrypoint.

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 JobFunction
JobFunction.spawn(**kwargs: Any) -> JobHandle

Submit 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.

JobFunction.submit(*args: object, **kwargs: object) -> JobId

Submit 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.