Scheduled jobs
A job runs a command in a fresh sandbox, once at a time you choose or on a cron schedule, and keeps each run's exit code and output. Use it for a nightly report, a cleanup every hour, or one task that should start at 03:00 without a machine of yours waiting to start it.
Terminalruntime job create nightly-report --cron "0 3 * * *" --timezone Europe/Berlin \ -- python3 /workspace/report.py # prints the job idruntime job create hello --at now -- bash -lc 'echo hello from a job'runtime job ls # state, schedule, next runruntime job logs "${job}" -f # its latest run's output, followedTypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();const job = await runtime.jobs.create({ name: "nightly-report", schedule: { cron: "0 3 * * *", timezone: "Europe/Berlin" }, command: ["python3", "/workspace/report.py"],});for await (const run of await runtime.jobs.runs(job.id)) console.log(run.id, run.state, run.exitCode);Pythonfrom withruntime import Runtimeruntime = Runtime()job = runtime.jobs.create("nightly-report", cron="0 3 * * *", timezone="Europe/Berlin", command=["python3", "/workspace/report.py"])for run in runtime.jobs.runs(job["id"]): print(run["id"], run["state"], run["exitCode"])Agents use the runtime_job MCP tool, with action create, list,
get, runs, run, logs, pause, resume or cancel.
Updated
On this page
What a run is#
Every run starts a new sandbox, runs the command in it and stops it. Nothing carries over from one run to the next, so a job that needs data reads it from somewhere that lasts, such as your own bucket or database, and writes its results back there.
- The command runs as given, without a shell. Pass an argv list. For pipes,
&&or variables, run it through bash:-- bash -lc 'cd /workspace && make'. A command is at most 16 KiB, andcwdmust be under/workspace. - The size is a sandbox's. 2 vCPU and 4 GiB with a 4 GiB disk unless you say otherwise, up to 16 vCPUs and 64 GiB. The disk is at least 3 GiB, the size of the system image.
- A run may last up to 60 minutes. The timeout is
30 minutes unless you set it, counted from when the command
starts. At the timeout the run is stopped, and every process it started with
it, and it ends
failedwith the reasontimeout. - Output is kept. A run keeps its last 256 KiB of standard output
and standard error together;
logsTruncatedsays when earlier output was dropped.
Schedules#
- Once:
--attakes an ISO time with its offset, such as2026-10-01T03:00:00Z, ornow. In the SDKs,atalso takes a date or Unix milliseconds; a time already past runs at once. - Recurring:
--crontakes the five cron fields, minute, hour, day of month, month and day of week, each*, a number, a list, a range or a step (*/15 * * * *is every fifteen minutes).--timezonetakes an IANA name such asAmerica/New_York, and is UTC unless you set it, so a schedule follows daylight saving time where you are. - Missed times run once. If runs were missed, for example while the service was down or while a run was still going, the job runs once for all of them and then keeps to its schedule.
- One run at a time. A job never has two runs going at once.
runtime job pause <id> stops new runs and resume starts them again; a run
already going finishes. runtime job cancel <id> ends the job for good and
stops a run in progress. A run that already finished keeps its result, and
cancelling a job twice changes nothing.
Failures and retries#
A run that exits with a code other than 0 has failed. By default a job tries
once. --attempts 3 --backoff 60 (in the SDKs, retry: { maxAttempts, backoffSeconds }) tries a failed run again in a fresh sandbox, up to
5 attempts per occurrence, waiting up to
60 minutes between them.
A run whose outcome Runtime could not see, because its sandbox stopped under
it, ends in state unknown. Runtime never runs that occurrence again, even
with retries set, because the command may have done its work; check what it
did before you run it by hand. The schedule carries on: the next occurrence
runs as usual.
If a run cannot start, the job's blockedReason says why and it tries again
every 30 seconds: credits (add credit), cost_cap (a limit you
set, or the key's daily limit, was reached), capacity (no room right now) or
permission (the key that made the job was revoked or narrowed).
What it costs#
A run is a paid sandbox, billed at the sandbox rates for exactly the time it
runs: $0.025 per active vCPU-hour and $0.0075 per reserved
GiB-hour, with nothing for the job itself. Each run holds its cost for its
time limit before it starts, and is charged only what it used, through the same
spending limits as every sandbox. The free trial's hours do not
fund jobs, so add credit first; a job made on an account without credit waits
with blockedReason credits.
Two limits bound what a job spends: --max-cost for one run and
--max-total-cost for every run of the job together (compute.maxCostMicros
and maxTotalCostMicros in the SDKs and API). An account runs up to
50 jobs; ask for more.
Secrets in a job#
A job puts secrets into its run's environment. Store the value once with
--jobs, then bind it by name when you create the job:
Terminalprintf %s "$DB_PASSWORD" | runtime secrets set DB_PASSWORD --jobsruntime job create backup --cron "0 2 * * *" --secret DB_PASSWORD=PGPASSWORD \ -- bash -lc 'pg_dump "$DATABASE_URL" | gzip > /workspace/backup.sql.gz'TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();const saved = await runtime.secrets.set("DB_PASSWORD", { value: "…", jobs: true });await runtime.jobs.create({ name: "backup", schedule: { cron: "0 2 * * *" }, command: ["bash", "-lc", 'pg_dump "$DATABASE_URL" > /workspace/backup.sql'], secrets: [{ name: "PGPASSWORD", secretId: saved.jobs.id }],});One runtime secrets holds both kinds of use. A secret can go to sandboxes,
to jobs, or both (--host and --jobs together), and the two copies behave
differently:
For sandboxes (--host) |
For jobs (--jobs) |
|
|---|---|---|
| What the code sees | A placeholder; the egress proxy adds the value on HTTPS | The value itself, in the variable you name |
| Size | Up to 8 KiB of plain text | Up to 64 KiB |
| Read it back | Never, by anyone | runtime secrets reveal NAME, with a key allowed to reveal |
| Change it | set again |
runtime secrets rotate NAME: a new version, the old one stops at once |
| Belongs to | The account | The person whose key stored it |
| Delete | runtime secrets rm NAME deletes both copies |
The same; the name cannot be used again |
A job takes up to 16 secrets. A run gets the value only while it is going, and a secret rotated or deleted after a run started is refused to that run. The value is in the run's environment, so a command that prints it puts it in the job's logs.
Commands#
| CLI | SDK | What it does |
|---|---|---|
runtime job create <name> ... -- |
jobs.create |
Schedule a job; prints its id |
runtime job ls |
jobs.list |
Your jobs, oldest first |
runtime job get <id> |
jobs.get |
One job, in full |
runtime job runs <id> |
jobs.runs |
Its runs: attempt, state, exit code |
runtime job logs <id> [-f] |
jobs.logs(runId, {cursor}) |
A run's output; a job id means its latest run |
runtime job pause, resume <id> |
jobs.pause, jobs.resume |
Stop or restart scheduling |
runtime job cancel <id> |
jobs.cancel |
End it and stop a run in progress |
The HTTP routes are POST /v1/jobs, GET /v1/jobs, GET /v1/jobs/{id},
GET /v1/jobs/{id}/runs, POST /v1/jobs/{id}:pause, :resume and :cancel,
GET /v1/job-runs/{id} and GET /v1/job-runs/{id}/logs. Every write takes an
Idempotency-Key; the SDKs and the CLI send one for you, so a retry after a
lost reply never makes a second job.
Try it on your own work. Sign up, then paste the setup prompt into your coding agent.