Docs

Schedules

Run a command every so often, or at a time of day, and have a stopped sandbox started for it and stopped again after.

For your agent

agental.io/docs/schedules.md
POST/v1/sandboxes/{id}/schedules
curl -X POST https://agental.io/v1/sandboxes/{id}/schedules \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"scrape","command":"python scrape.py","every":"1h"}'
  • command is one line, run by sh -c, with the variables and files any command of the sandbox has.
  • every is a number and m, h or d, from 5m to 31d, counted from when the schedule is made: what has to run more often is better left running in a sandbox that stays up. Or give cron, five fields: minute, hour, day of the month, month and day of the week, such as "0 7 * * 1-5", with no two runs less than 5 minutes apart, read in timeZone, such as "Europe/Sofia", UTC unless it says. Or give at, a date and time up to a year away, and it runs once: a check to come back to, such as a deploy at 09:00. Afterwards its nextRunAt is null, and it is kept, with its run, until it is deleted.
  • The answer is 201 with the schedule, its nextRunAt, and lastRun, null until it has run. name is lower-case letters, digits and dashes, unique on the sandbox: left out, it is schedule-1, schedule-2 and so on. A sandbox has 20 at most.

When the sandbox is stopped

A run that finds the sandbox stopped starts it, runs the command, and stops it again when the command ends, keeping its disk. The sandbox is billed by the second for the minutes it runs, and at the stopped price between runs: an hourly job of two minutes comes to about a tenth of what the same sandbox costs left running. A run is made within about 15 seconds of its time, and a stopped sandbox takes some seconds more to start. A cron's or a one-off's time is also given the schedule's own delay of up to a minute, the same each time and in its nextRunAt, so that the schedules due at 07:00 do not all start their sandboxes in the same moment.

FieldWhat it does
ifStoppedstart, the default: start it for the run, then stop it again. skip: a run that finds it stopped is skipped, so the schedule runs only while the sandbox runs.
keepMemorytrue stops it with its memory kept, at the stopped price with memory, so the next start puts back its processes as they were. A sandbox that was stopped with its memory is stopped so again, whatever this says.

A run leaves the sandbox running when someone else began to use it meanwhile: it was started again, its stop was moved, or a command was sent into it by /exec, /exec/stream or /commands. An SSH session or the desktop is not seen: move the stop, as with PUT /v1/sandboxes/{id}/auto-stop, to keep it up. When two runs go in one sandbox, the last one to end stops it.

A schedule that starts its sandbox takes a key that may start and stop it, sandbox.resume and sandbox.stop, as well as exec; one with ifStopped: "skip" takes exec alone. Another key is answered 403 key_action_forbidden.

Delete the sandbox after

curl -X POST https://agental.io/v1/sandboxes/{id}/schedules \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"last-run","command":"./finish.sh","at":"2026-11-04T09:00:00Z","deleteAfter":true}'
  • A schedule that runs once, with at, may delete its sandbox after its run: deleteAfter: true. Once the command exits 0, the sandbox is deleted as DELETE /v1/sandboxes/{id} deletes it, whether the run started it or it was running already, and the run's deletedSandbox is true. Its sandbox.deleted event has reason: "schedule".
  • A run that fails, times out, is skipped, is replaced, could not start or is cut short by a restart of the API leaves the sandbox, stopped again if the run started it, so that you can look at what went wrong. Its reason says the sandbox was not deleted.
  • deleteAfter on every or cron is refused with 400 invalid_request, and so is a change that makes a one-off with it run again and again: give deleteAfter: false with it. Making, changing or running now a schedule with it takes a key that may delete the sandbox, sandbox.delete, or sandbox.delete-own for one the key made, as the delete itself does. Those rights are checked then, not when the run ends: a schedule acts for the account, so it still deletes the sandbox after the key that made it is revoked or the member who made it leaves. Delete the schedule, or give it deleteAfter: false, to stop that.
  • A scheduled run still going in the sandbox when it is deleted ends as failed. The schedule and its runs can be read until the sandbox's history is cleared. From a terminal, agental schedule add <sandbox-id> --at <date-time> --delete-after -- ./finish.sh.

When a run is still going

For a schedule that runs again and again: a one-off's run never finds one of its own still going.

overlapThe run that comes due
skip, the defaultis skipped, and the one going goes on.
replacestops the one going and takes its place.
parallelruns beside it, five of one schedule at most.
  • A run gets timeoutMs, 600000 (10 minutes) unless it says, up to 21600000 (6 hours). One that runs out is given up with exit code 124.
  • A run that could not be made within two minutes of its time, as while the sandbox was being moved to another server, is told as missed and never made up later. One under way when the API restarts is interrupted, and a sandbox it started is stopped.
  • After 10 runs one after another failed or timed out, the schedule pauses itself, so that a broken command does not start and bill its sandbox on and on. pausedReason says why, and the event sandbox.schedule_paused is sent. A resume, {"enabled":true}, starts it again and clears the reason. Skipped and missed runs, and runs that could not start the sandbox, which cost nothing, count neither way; nor do the failures before a pause or a resume. An at names its zone, as Z or +02:00.

Its runs

GET/v1/sandboxes/{id}/schedules/{scheduleId}/runs

Its runs, the latest first, 100 unless limit says, at most 500. Each has dueAt, startedAt and finishedAt, outcome, exitCode, startedSandbox, stoppedSandbox (disk, memory or null), deletedSandbox and commandId, whose output GET /v1/sandboxes/{id}/commands/{commandId} gives. A run's reason says, in words, why it ended as it did where the outcome does not.

outcomeWhat it means
runningIt is going.
succeeded, failed, timed_outIts command exited 0, exited with another code, or ran out of time.
skippedThe sandbox was stopped and the schedule skips then, or the last run was still going.
replacedA later run of the same schedule took its place.
missedIt could not be made on time.
could_not_startThe sandbox could not be started: its reason says why, such as the account's plan.
interruptedThe API restarted while it ran.

Each finished run is also the event sandbox.scheduled_run, sent to the webhooks that take it, with the run's outcome and exit code, and a schedule that pauses itself is sandbox.schedule_paused.

Run now, pause, change, delete

  • POST /v1/sandboxes/{id}/schedules/{scheduleId}/run makes a run now, by the schedule's own rules, paused or not, and answers 202 as it begins. Its next run does not move.
  • PATCH /v1/sandboxes/{id}/schedules/{scheduleId} changes what it is given and keeps the rest. {"enabled":false} pauses it; a change of when it runs, or a resume, sets its next run from now. A run under way goes on as it began, save that turning deleteAfter off while it runs keeps its sandbox; turning it on does not delete the sandbox after a run already under way.
  • DELETE /v1/sandboxes/{id}/schedules/{scheduleId} deletes it with its runs. GET /v1/sandboxes/{id}/schedules lists a sandbox's, each with its last run.
  • From a terminal, agental schedule add <sandbox-id> --every 1h -- python scrape.py, and agental schedules <sandbox-id> lists them. In the console, a sandbox's Schedules view.

The full machine-readable reference is OpenAPI 3.1. Every page as Markdown, for an agent: llms.txt. Questions: [email protected]