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 run is kept in the task's history with its outcome, what triggered it and, when it fails, the reason the cluster gave. The run that failed overnight is still there in the morning.
Logs are the exception. While a run is in progress its pod logs stream in the project's logs view, but they are not retained after the run finishes and its pod is gone. A task whose output you need later should write it somewhere that keeps it.
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 —
kajaruns tasks too, andrun_commandin the MCP server is the same machinery.