Skip to main content
A job runs a command on a cron schedule for one app: a database migration, a cache warm-up, a nightly report. Every run is recorded with its start, end, exit code, duration, trigger and node, and its output is kept so you can read it later from the dashboard or the CLI. Jobs live on the app’s Jobs tab, under levelrail-cli jobs, in app.yaml, and at /api/v1/apps/{name}/jobs. They are the same jobs on every surface.

Where a job runs

A fresh job gets no published ports and no restart policy, and only the volumes the app already declares. It runs as the image’s user under the same Docker guard rules as the app itself. On a multi-node setup the job runs on the node the app is placed on. The command is a real argv list with no shell involved. Use ["sh", "-c", "..."] for pipelines. The dashboard form wraps what you type in sh -c for you. For a fresh job the command replaces the image’s entrypoint, so what you write is exactly what runs.

Create a job

Dashboard

Open the app, choose Jobs, then Create job. Pick the schedule (daily, weekly or a cron expression), the time zone, and the policies below. Use Run now on a row to try it immediately.

CLI

<job> is the job’s name or its ID. jobs logs prints the latest run’s output, or a specific one with --run RUN_ID.

app.yaml

Every deploy makes the service’s app.yaml jobs match the list: new ones are created, changed ones updated in place (keeping their history), and ones you remove are deleted. A spec job is marked app.yaml in the dashboard and can only be enabled or disabled there. Jobs you create in the dashboard or CLI are never touched. A spec with no jobs: key leaves existing jobs alone; jobs: [] removes every spec job. Jobs created through the API default to concurrency: allow.

When jobs run, and what survives a restart

The scheduler is level-triggered: each tick it works out which cron occurrence is due from the job’s saved high-water mark and the clock, claims that occurrence in the database, and only then runs it. Because the claim is saved before the command starts:
  • A control plane restart never runs the same occurrence twice.
  • A run that was in flight when the control plane stopped is marked interrupted and its container is removed. It is not retried, since the command may already have done real work.
  • If the control plane was down across an occurrence, the job is either run once (catchUp: run-once, when the occurrence is inside the lookback window) or recorded as missed (skip). Nothing is dropped silently, and a job that was only just enabled does not backfill.
Time zones follow daylight saving: a wall time that does not exist on a spring forward day runs once at the shifted time, and a repeated wall time on a fall back day runs once.

History and logs

The Jobs tab shows the run history for the app, newest first, with a status filter and a log view per run. The same data is at GET /api/v1/apps/{name}/job-runs and GET .../jobs/{job}/runs/{run}. Each run records the trigger (schedule, manual or api), attempts, exit code, duration and node. Output keeps the last 64 KiB of stdout and stderr. Values of the app’s secrets (and any env var whose name looks like a password, token, key or DSN) are replaced with [redacted] before the output is stored. Output of a fresh job is captured on the control plane’s own node. On a remote node the run still records its exit code and timing, but not output; use exec mode when you need the output from a remote node.

Alerts

Create alert rules on a job from Alerts or levelrail-cli apps alerts create with --scheduled-task-id <job id>: They use the same notification channels as every other alert.

Limits and settings

Examples

Run migrations nightly and alert if they fail twice in a row:
Warm a cache every 15 minutes with a small memory cap:
Email a report every Monday morning, and catch up if the server was rebooting:
Last modified on October 12, 2026