Portaldocs
Run and watch

Run a job

Compare quotes, launch a Python script on the machine you choose, and set its project, environment, secrets, budget and policies.

Warnings and limits

  • A launch uploads the directory you run it from. Files over 100 MB are left out, a project over 500 MB is refused, and files that may hold credentials (.env files, private keys, cloud credential files and directories) never go up.
  • --project NAME is required to launch: 1 to 100 characters.
  • A workspace runs at most 100 jobs at once (queued and running jobs count, suspended ones do not); a launch or a resume past that is refused with 429.
  • Keep secrets out of --env: its values are kept with the job's spec, not sealed like a stored secret.
  • A budget is a limit, not an estimate: the job is suspended (or stopped) a little before it is spent, and a job whose budget is spent cannot be resumed or moved.
  • Ctrl-C detaches from the output. It does not stop the job; portal stop JOB does.
  • Project requirements that are not pinned resolve when each machine boots, so two machines can install different versions. Pin them when that matters.
  • The job's environment installs Portal's own dependencies (dill, cloudpickle and the rest) as compatible ranges, so your pins win. Requirements that cannot resolve together fail the environment build, and the job's error carries uv's reason.
  • On Modal the environment is an image built on first use, 30 minutes at most for the requirements; a build that fails is built once more afresh before the job fails.

Quote, then launch

Without --on, portal run only compares quotes. Nothing is uploaded and nothing starts:

portal run train.py --gpu T4

Pick a quote and launch it with its exact spec:

portal run train.py --gpu T4 --on SPEC --project training -d
OptionWhat it does
--on SPEClaunch on exactly this offer, as a quote printed it
--project NAMEthe project the job belongs to: required to launch; portal ps and the console group jobs and their costs by it
--name NAMEa name to refer to the job by (1 to 100 characters)
-d, --detachreturn once the job is queued, instead of following its output
--planonly show quotes, even with --on

Requirements narrow the quotes: --gpu takes a model (T4, L4), a memory per GPU (24G) or any; --gpus N, --cpus N and --mem GB set minimums; --spot asks for spot only and --no-spot for none (by default spot is used when offered); --providers aws,modal limits the providers. Arguments for your script go after --:

portal run train.py --gpu T4 --on SPEC --project training -- --epochs 10 --lr 3e-4

A job launched with an exact --on stays on that machine type: Portal does not move it to a cheaper offer or back to spot. Notices, out-of-memory rescues, provider time limits, budgets, idleness and the time limit still apply. See What Portal does on its own.

What is uploaded

The directory you run portal run from, with the script in it, is packed and uploaded. Left out:

  • .git, virtual environments, __pycache__, compiled Python and tool caches;
  • whatever your .gitignore files leave out;
  • files that may hold credentials: .env and *.env files, .envrc, private keys and certificates, .netrc and similar files, .aws, .ssh, .config/gcloud and similar directories, and JSON files holding a "private_key";
  • files over 100 MB;
  • links that point outside the directory.

A .portalignore file uses .gitignore's syntax on top of them: a pattern leaves more out, and a !pattern brings a path back, including a credential file you name exactly or a file over 100 MB. A project over 500 MB in all is refused: keep large data in storage your script downloads.

Paths

Your script runs on the machine in a copy of the uploaded directory, which is its working directory. Relative paths behave as they do on your computer. Write what you want to keep inside that directory: it is the job's output when the job ends. See Outputs and logs.

Python and requirements

Portal reads what your project declares, in this order: requirements.txt; else pyproject.toml's [project].dependencies; else your uv.lock; else the imports of your .py files, mapped to the packages that provide them. Lines that point at files on your computer (-e, -c, local paths) are left out.

The job runs on Python 3.12, or 3.13 or 3.14 when .python-version names that minor version. Other versions are refused. GPU jobs get PyTorch's CUDA 12.6 build unless your requirements pin another build (+cu128, +cpu) or name a PyTorch index.

Environment variables and secrets

portal run train.py --on SPEC --project training --env BATCH=64 --env MODE=fast

--env K=V sets a variable for the script. Its value is kept with the job's spec, so never use it for a secret.

For a secret, store it once and pass it by name:

portal secrets set HF_TOKEN                    # asks for the value, without showing it
portal secrets set HF_TOKEN --from-env HF_TOKEN
portal run train.py --on SPEC --project training --secret HF_TOKEN

The value is read privately from the terminal or standard input, never from an argument. A workspace keeps at most 64 secrets, 16 MiB sealed in all. Storing or deleting a secret needs a Clusy sign-in from the last 5 minutes. A secret name is letters, digits and _, does not start with a digit, and is at most 128 characters.

Stored secrets in runs

Hosted Portal refuses a run that names stored secrets today, because the CLI and coding tools never hold the admin scope that check asks for. Until it is fixed, a script that needs a credential has to get it another way, such as your own secret manager. See Known issues.

Budgets

portal run train.py --on SPEC --project training --budget 25

--budget USD is optional. Portal acts a little before the job's estimated cost reaches it: when cost plus 2 minutes of the machine's price would pass it, the job is suspended, its machine released, and it waits until someone resumes it. A suspension that fails stops the job. A job whose budget is spent is refused a resume or a move. See Spend caps and budgets.

What Portal tells you, before and after

Quotes come with warnings when there is something to know before you go ahead, and the job's events repeat them: the account's quota for the machine type when it admits only one such machine at a time ("the account's vCPU quota L-DB2E81BA in us-east-1 is 4 vCPUs: 1 g4dn.xlarge at a time"), or that the quota could not be read; a price that is an estimate rather than an exact quote; the budget a fixed-limit provider reserves up front (see Spend caps and budgets); and, in the job's provisioning event, how long the machine usually takes to boot. A spot machine's warning says what its notice can save of the job's memory. When the job ends, its last events name where its output is (portal logs JOB for the log, portal pull JOB for the files, kept 7 days) and the cost, saying when part of it is unknown, and the saving.

Policies for one job

OptionWhat it does
--no-cheapernever move to a cheaper offer on its own
--no-returnnever move back to spot when that pays
--no-evacuateno checkpoint and suspension on a spot notice
--rightsize on_move|active|offhow CPU and memory follow the job's measured use
--policy KEY=VALUEany policy setting, such as idle.after_min=60

The time limit (ttl.hours, 72 by default, for example --policy ttl.hours=24) counts the time machines run the job: a running job is stopped once it has run that long, and time suspended does not count toward it. A suspended job has a limit of its own, the same number of hours counted from the job's creation on the clock: past it the job ends ("ended at its time limit of 72 h while suspended"), its checkpoint is kept for 7 days, and portal resume JOB continues it from there within those days. See Suspend and resume.

A portal.yaml in the project can hold the same policy settings; flags take precedence over it. See Settings.

When the GPU you asked for is not available

A GPU can be scarce: Modal schedules an L4 when one frees up, and EC2 refuses a type a zone has run out of. Portal waits a bounded time (5 minutes on Modal, EC2's capacity rounds), says what it is waiting for in the job's events every minute, and then ends the job as no_capacity with choices: that provider's other GPUs with their prices, the same machine on-demand, or spot elsewhere. It never picks another machine on its own.

If you would rather the job ran than waited for your choice, say so up front:

portal run train.py --on modal:gpu=L4 --gpu-fallback same_provider --project training
portal run train.py --on modal:gpu=L4 --gpu-fallback any --project training

same_provider runs on that provider's cheapest other GPU of the job's size (its GPU count and the memory per GPU you asked for); any on the cheapest GPU of any provider your requirements allow. The quotes say up front what that would be and what it costs, more or less than the GPU you asked for ("gpu_fallback=same_provider: if L4 (modal, $0.800/h) is not scheduled within Portal's bounded wait (5 min on Modal), the job runs on T4 (modal, $0.590/h) instead, $0.210/h less, and is pinned there"), and the job's events then say exactly what happened: "L4 not scheduled in 5 min on modal; running on T4 (modal, $0.590/h) instead, as asked by gpu_fallback=same_provider: $0.210/h less than L4's $0.800/h; the job is pinned there (portal move moves it)". A job that fell back is pinned to that machine, whether or not you launched it with --on, so no automatic move takes it back to the GPU that was not there; portal move moves it when you want. The fallback itself gets no fallback: if that GPU is not available either, or the job's budget cannot pay for it, the job ends with the choices for the GPU you asked for. portal move JOB --to SPEC --gpu-fallback MODE does the same for a move.

From a coding tool

The MCP tool run takes the same choices: the script, its arguments, requirements, on, project, env, secrets, budget_usd, gpu_fallback and policies. It uploads only a directory inside the one the MCP server started in, or one you added with portal mcp --root DIR. See MCP tools.

On this page