On this page
Tasks & scheduled jobs
An app is a container that keeps running. A task is a container that runs to completion and stops: a nightly report, a cleanup, a backfill, a migration.
Most tasks are an app you already have, with a different command. So a task can name an app and inherit its image, variables, secrets, resources and registry credentials — which means adding a nightly job to an existing service is one field, not a second copy of that service's configuration. A task can also name an image directly, for work that does not belong to any app.
On a schedule#
Give a task a cron expression and it runs on that schedule. A timezone can be set alongside it; without one, the cluster's own zone applies.
A scheduled task can be suspended, which stops it firing without deleting it or losing its history — the right move when a job is misbehaving at 3am and you want it to stop now and be fixed later.
On demand#
Any task can be run immediately, whether or not it has a schedule. A task with no schedule at all is a perfectly normal thing to have: it is the migration or the backfill you run by hand, kept somewhere you can find it again instead of pasted into a terminal.
Every task has a page of its own in the project, with an Overview (what it runs, its schedule and next run, its recent runs), a Runs tab and its Settings. Run now, pause, resume, duplicate and delete are on every one of them.
Every run is kept with its outcome, what triggered it, how long it took, its exit code and, when it fails, the reason in words: out of memory, a non-zero exit, an eviction, an image that could not be pulled. The run that failed overnight is still there in the morning, and each run has a page of its own with its logs. While it runs they stream live; afterwards they are read from the cluster's log store, which needs Observability installed on the cluster. Without it a finished run's logs go with its pod, and the run page says so.
A one-off command run through the MCP server's run_command is recorded as a task
too, so it has the same history, and the task list keeps those behind a Show one-off runs
toggle rather than beside the tasks you defined.
Bounding a run#
| Control | Default | Why it exists |
|---|---|---|
| Timeout | — | A wedged run otherwise holds its slot forever, and every later run is skipped. |
| Retries | 0 | How many times a failed run is retried before it counts as failed. Zero is the right answer for most migrations. |
| Overlap policy | Refuse | What to do when the next run is due and the last one is still going. Overlapping runs of the same job are usually a data hazard rather than more throughput. |
| History limit | 3 | How many finished runs of each outcome to keep. |
The pre-deploy command#
Separate from tasks, and easy to confuse with them: an app can carry a pre-deploy command.
It runs once per version, with the incoming image, before that version reaches the app. If it exits non-zero the rollout stops and the previous version carries on serving. This is where database migrations belong — the migration runs with the code that needs it, and a broken migration means a failed deploy rather than a running app talking to a schema that does not match it.
It is a command on the app rather than a reference to a task, deliberately. A reference can dangle, and a dangling reference here would block every deploy with no way to express the fix. If you want that same command runnable by hand as well, make it a task too.
In a preview environment, the pre-deploy command is what turns a fresh, empty database into one the branch can actually run against.
Next steps#
- Preview environments — where an empty database and a migration meet.
- Apps — where the pre-deploy command lives.
- CLI —
anyportruns tasks too, andrun_commandin the MCP server is the same machinery.