Jobs
Jobs are finite workloads in Quave ONE. Use them for customer-visible, one-off or repeatable commands such as Rails migrations, database maintenance scripts, data imports, and backfills.
A Job is a first-class app type, separate from Apps, Databases & Services, and Functions. Each execution creates a JobRun with its own status, command snapshot, timing, logs, and exit-code metadata.
How Jobs Work
- Finite execution: Each run creates a Kubernetes Job instead of a long-running Deployment.
- No public host by default: Jobs do not need a port, hosts, HTTP probes, or a health check.
- Visible history: Every run is stored as a
JobRun, so customers can see what command ran, when it started, whether it succeeded, and which logs it produced. - Environment variables: Runs use runtime variables saved on the Job environment. The API, CLI, and MCP tool can merge variables into that environment immediately before a run.
- Normal resource usage while running: Job pods consume account resources while they run. Finished Kubernetes Jobs are cleaned up by their TTL.
- Safe config changes: Editing a saved Job command creates pending changes. The new command is not used by default runs until you apply those changes.
Common Use Cases
- Rails or Ruby migrations, for example
bin/rails db:migrateorbundle exec rails db:migrate - One-off scripts, for example
bundle exec rake users:backfill - Data imports or exports
- Maintenance tasks that should not restart an app pod repeatedly when they fail
- Repeatable customer operations that need logs and audit history
Create a Job
Via the Web UI
- Open the account in Quave ONE.
- Go to Apps.
- In the Jobs section, click Add new Job.
- Choose the deployment source, usually Use image for a pre-built image or CLI deployment for source builds.
- Set the Job app name, image or build configuration, region, and environment.
- Open App Config and configure Job Defaults.
- Open the environment and select Settings → Job Config if that environment needs a command or timeout override.
- Deploy the image or build the content before creating the first run.
Via the Public API
Create the app with dockerPreset set to JOB. A Job app does not require port.
curl -X POST \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"name": "Rails migrations",
"accountId": "ACCOUNT_ID",
"dockerPreset": "JOB",
"useImage": true,
"image": "ghcr.io/acme/my-rails-app:2026-06-19",
"jobConfig": {
"command": "bin/rails db:migrate",
"shell": true,
"timeoutSeconds": 1800,
"ttlSecondsAfterFinished": 21600,
"backoffLimit": 0
},
"isCliDeployment": true
}' \
https://api.quave.cloud/api/public/v1/app
Then create an environment for that app:
curl -X POST \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"accountId": "ACCOUNT_ID",
"appId": "APP_ID",
"name": "staging",
"region": "us-5",
"zClouds": 1
}' \
https://api.quave.cloud/api/public/v1/app-env
If you deploy a pre-built image, deploy it to the environment before running the Job:
curl -X POST \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"appEnvId": "APP_ENV_ID",
"image": "ghcr.io/acme/my-rails-app:2026-06-19"
}' \
https://api.quave.cloud/api/public/v1/app-env/deploy-image
Via MCP
Ask your AI agent to create and configure the Job:
"Create a Job app called Rails migrations using image ghcr.io/acme/my-rails-app:2026-06-19 and command bin/rails db:migrate"
Useful MCP tools include:
| Tool | Purpose |
|---|---|
create-app | Create the Job app with dockerPreset: "JOB". |
create-app-env | Create the environment for the Job app. |
deploy-app-env-image | Deploy a pre-built image to the Job environment. |
update-app-env-job-config | Edit environment-level Job execution settings. |
get-app-env-job-schedule | Inspect the environment schedule, next run, and recent occurrences. |
update-app-env-job-schedule | Configure, enable, or disable one recurring schedule immediately. |
apply-app-env-changes | Apply pending Job settings to the environment. |
run-app-env-job | Optionally merge environment variables and create a new JobRun; can wait briefly for a concurrency slot with waitForConcurrencySlot. |
list-app-env-job-runs | List previous runs with page, status, and time filters. |
get-app-env-job-run | Check one run. |
get-app-env-job-run-logs | Fetch logs for one run. |
cancel-app-env-job-run | Cancel an active run. |
rerun-app-env-job-run | Rerun from a previous run snapshot; can wait briefly for a concurrency slot with waitForConcurrencySlot. |
If a Job environment is at its active-run limit, Quave ONE returns actionable concurrency details including the configured limit, active count, active JobRun IDs when available, and suggested actions. Automation callers can set waitForConcurrencySlot: true to wait through short-lived races before failing. The default wait is 300 seconds; set concurrencySlotTimeoutSeconds to override it up to 1800 seconds.
Find Job settings in the dashboard
Job configuration is split by scope and behavior:
| Location | What it controls | Save behavior |
|---|---|---|
| App Config → Job Defaults | Default command and execution settings inherited by every environment. | Creates pending changes for the Job environments. |
| Environment → Settings → Job Config | Command and execution overrides for one environment. This is the first Settings subtab for Jobs. | Creates pending changes for that environment. |
| Environment → Settings → Schedule | One recurring schedule, its next occurrence, status, and recent occurrences. This is the second Settings subtab for Jobs. | Saves immediately; it does not use Apply changes. |
| Environment → Settings → CLI Instructions | Environment name, deployment commands, and CLI guidance. This is the third Settings subtab for Jobs. | Read-only instructions. |
Opening Settings on a Job environment defaults to Job Config.
Job Config
Saved Job execution settings are stored in jobConfig.
| Field | Description |
|---|---|
command | Command to execute. Required before a run starts unless each run provides an override. |
args | Optional argument list. If shell is true, arguments can also be included directly in command. |
shell | Runs the command through /bin/sh -lc when true. Default: true. |
workingDir | Optional working directory inside the container. |
timeoutSeconds | Active deadline for a run. Default: 1800 seconds. |
ttlSecondsAfterFinished | Kubernetes TTL after completion. Default: 21600 seconds. Minimum: 300 seconds. |
backoffLimit | Kubernetes Job retry backoff limit. Default: 0. |
maxConcurrency | Maximum active runs for this Job environment. |
allowConcurrentRuns | Allows unlimited active runs when true and maxConcurrency is omitted. Default: false. |
Per-run overrides only accept the safe execution fields: command, args, shell, workingDir, timeoutSeconds, ttlSecondsAfterFinished, and backoffLimit. Configure concurrency on the app or environment. Recurring schedules are stored separately on each environment, not inside jobConfig.
Schedule a Job
Each Job environment can have one optional recurring schedule. In the dashboard, open the environment and select Settings → Schedule.
When recurring runs are disabled, this subtab shows only Enable recurring runs. Select it to reveal the schedule form, then choose a preset or enter an exactly five-field cron expression, choose an IANA time zone, and confirm the next-occurrence preview. Select Save schedule to persist and enable the recurring run. Clearing the checkbox on an enabled schedule disables future runs immediately.
When the saved schedule is enabled, the same subtab also shows schedule status and Recent occurrences. Each occurrence includes its planned time and scheduler outcome, plus either a safe reason or a link to the normal Job run when one was created.
Schedule changes are immediate control-plane operations. They do not create pending deployment changes and do not require Apply changes. Enabling a schedule calculates only the next future occurrence. Disabling prevents future scheduler claims immediately, but it does not cancel a JobRun that was already created.
The schedule is not a separate app type and does not create a Kubernetes CronJob. The execution path stays:
schedule occurrence -> Quave ONE JobRun -> Kubernetes batch/v1 Job
This keeps manual, API, MCP, and scheduled runs in the same history, concurrency, logs, cancellation, billing, and audit model. Each due time is first recorded as a durable occurrence. Executable occurrences create one JobRun with trigger type SCHEDULED; missed or skipped occurrences remain visible without a synthetic run.
Cron and time-zone rules
- Use exactly five fields: minute, hour, day of month, month, and day of week.
- Seconds fields and aliases such as
@dailyare rejected. - Use an IANA time zone such as
UTC,America/Sao_Paulo, orEurope/London. - The computed next occurrence follows the chosen time zone, including daylight-saving transitions.
Execution and missed-run behavior
A scheduled run uses the ready content and applied Job execution configuration available when the occurrence is claimed. Unapplied command or timeout edits are not used. Like an ordinary run without an explicit envVars override, the executor reads the Job environment's current runtime variables.
Scheduled runs obey the same concurrency limit as manual and API runs. When content is not ready, the applied configuration is not runnable, or the concurrency limit is reached, the occurrence ends with a safe SKIPPED result and no JobRun. Dispatcher failures end as FAILED instead of retrying the same occurrence forever.
The Jobs service normally dispatches every minute. An occurrence up to five minutes late may run. After a longer scheduler interruption, Quave ONE records one coalesced MISSED occurrence for the outage interval, does not burst-run every elapsed time, and advances to the next future occurrence.
Configure with the Public API:
curl -X PATCH \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"appEnvId": "APP_ENV_ID",
"expectedRevision": 0,
"enabled": true,
"cronExpression": "0 * * * *",
"timeZone": "UTC"
}' \
https://api.quave.cloud/api/public/v1/app-env/job-schedule
Inspect the schedule and its recent occurrences with GET /api/public/v1/app-env/job-schedule?appEnvId=APP_ENV_ID, then send the returned schedule.revision as expectedRevision when updating. This rejects stale edits instead of overwriting another user's newer schedule. You can also use the MCP tools get-app-env-job-schedule and update-app-env-job-schedule.
Config Hierarchy and Apply Changes
There are four layers of Job configuration:
- App defaults: Set on the Job app as
jobConfig. - Environment overrides: Set on one app environment as
jobConfig. - Applied deployment snapshot: Captured from the app and environment settings when the environment image/build is deployed or when pending Job settings are applied.
- Per-run override: Passed when creating one
JobRun; it only affects that run.
By default, new JobRuns use the applied deployment snapshot. If a Job environment does not have an applied snapshot yet, Quave ONE falls back to the current app and environment settings. This means:
- Editing Job Defaults on the app creates pending deploy changes for the environments of that app.
- Editing Settings → Job Config on the environment creates pending deploy changes for that environment.
- A default run keeps using the previous applied command until you click Apply changes, call
POST /api/public/v1/app-env/apply-changes, call the MCP toolapply-app-env-changes, or save withapplyImmediately: true. - Passing a per-run
jobConfigtorun-app-env-joborPOST /api/public/v1/app-env/job-runsoverrides the applied snapshot for that run only. - Historical
JobRunrecords are immutable. Changing the app or environment config does not rewrite older run snapshots.
Environment tokens can save pending Job config changes when scoped to that environment, but applying changes immediately requires a user token.
Run a Job
Create a run with the saved applied command:
curl -X POST \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"appEnvId": "APP_ENV_ID"
}' \
https://api.quave.cloud/api/public/v1/app-env/job-runs
Or override the command for one run:
curl -X POST \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"appEnvId": "APP_ENV_ID",
"envVars": {
"RAILS_ENV": "production",
"DATABASE_URL": "postgres://database.example/app"
},
"jobConfig": {
"command": "bin/rails db:migrate",
"timeoutSeconds": 1800,
"backoffLimit": 0
}
}' \
https://api.quave.cloud/api/public/v1/app-env/job-runs
The response includes jobRunId, status, the command snapshot, timing values, and the content version used for the run.
Variables passed in envVars are merged into the saved Job environment only after the Job, content, command, and concurrency preflight succeeds. They are not ephemeral overrides: existing variables not included remain unchanged, plain values keep an existing variable's type and secret setting, structured values can explicitly change that metadata, new plain variables default to secret runtime variables, and future runs reuse the saved values. The configuration merge remains durable even if a later enqueue step fails. An empty map is treated as no change. A user token must belong to an account admin to change variables; a matching environment token is already restricted to that environment and can also perform the merge.
The run stores a runtime variable snapshot after the merge, with secret values encrypted. The executor reads environment variables from that exact snapshot, which keeps a queued JobRun immutable even if the saved Job environment is edited again before Kubernetes starts it. BUILD-only variables remain saved on the environment but are excluded from the Job container; use DEPLOY or BOTH for runtime values. The snapshot is internal and JobRun responses never expose variable values.
From the CLI, pass the same variables with repeatable --env-var flags:
quaveone job run \
--env acme-rails-migration-prod \
--image "$IMAGE" \
--env-var RAILS_ENV=production \
--env-var DATABASE_URL="$DATABASE_URL" \
--command "bin/rails db:migrate" \
--wait
Check Status and Logs
In the dashboard, open a Job environment and use the Runs tab to run the Job, refresh previous runs, page through history, filter by status/time, open run details, fetch logs, rerun a snapshot, or cancel an active run.
List recent runs:
curl -X GET \
-H 'Authorization: YOUR_TOKEN' \
'https://api.quave.cloud/api/public/v1/app-env/job-runs?appEnvId=APP_ENV_ID&page=1&limit=20'
Get one run:
curl -X GET \
-H 'Authorization: YOUR_TOKEN' \
'https://api.quave.cloud/api/public/v1/app-env/job-run?jobRunId=JOB_RUN_ID'
Fetch logs:
curl -X GET \
-H 'Authorization: YOUR_TOKEN' \
'https://api.quave.cloud/api/public/v1/app-env/job-run/logs?jobRunId=JOB_RUN_ID&stream=INFO'
Job statuses are QUEUED, STARTING, RUNNING, SUCCEEDED, FAILED, CANCELED, TIMED_OUT, and NOT_FOUND. Treat only SUCCEEDED as a successful terminal status. FAILED, CANCELED, TIMED_OUT, and NOT_FOUND should block a release pipeline.
Rails Migration Pattern
For a Rails migration:
- Build or publish the same image version that your app should migrate.
- Create a Job app with
dockerPreset: "JOB"and that image. - Set the app default command to the Rails command that exists inside your container, usually
bin/rails db:migratefor apps with Rails binstubs orbundle exec rails db:migrate. - Create one environment per target, for example staging and production.
- Deploy the image to the target Job environment.
- Apply pending Job setting changes if you edited the command after the deploy.
- Run the Job.
- Poll the
JobRununtil it is terminal. - Read the logs and exit status.
Recommended config:
{
"command": "bin/rails db:migrate",
"shell": true,
"timeoutSeconds": 1800,
"ttlSecondsAfterFinished": 21600,
"backoffLimit": 0
}
Use backoffLimit: 0 when a migration should not retry automatically after a failure. If you want to run it again, use rerun-app-env-job-run or create another JobRun after checking the logs.
Gate an App Deploy on a Rails Migration
A Rails release usually needs the migration to finish before the web app rollout continues. In Quave ONE, use the same image tag for the migration Job and the app deploy, then poll the JobRun status before deploying the app.
For a complete GitHub Actions workflow, compare this sequence with the public demo repository quaveone/rails-migration-job-demo.
The API flow is:
- Deploy or register the new image on the migration Job environment.
- Create the JobRun and save the returned
jobRunId. - Poll
GET /api/public/v1/app-env/job-run?jobRunId=JOB_RUN_IDuntilisTerminalis true. - Continue only when
statusisSUCCEEDEDorisSuccessis true. - If the status is
FAILED,CANCELED,TIMED_OUT, orNOT_FOUND, fetch logs and stop the app deploy. - Deploy the web app image after the migration succeeds.
The CLI supports this as a pipeline-friendly sequence:
IMAGE="ghcr.io/acme/rails-app:${GITHUB_SHA}"
quaveone job run \
--env acme-rails-migration-prod \
--image "$IMAGE" \
--env-var RAILS_ENV=production \
--env-var DATABASE_URL="$DATABASE_URL" \
--command "bin/rails db:migrate" \
--wait \
--timeout-seconds 1800 \
--backoff-limit 0 \
--logs-on-failure
quaveone deploy \
--env acme-rails-web-prod \
--image "$IMAGE" \
--wait
In CI, keep these as separate ordered steps or join them with shell &&. Because quaveone job run --wait exits non-zero for every terminal status except SUCCEEDED, the app deploy step will not run when the migration fails.
Quave ONE can gate the deploy, but Rails migrations should still be written with normal zero-downtime practices: prefer backward-compatible schema changes, avoid assuming all old pods have stopped, and inspect the JobRun logs before retrying a failed migration.
Rerun and Cancel
- Rerun uses the previous run's immutable config snapshot and content by default. You can override
contentIdor provide a new per-runjobConfig. - Cancel deletes the Kubernetes Job for an active run and marks the
JobRunasCANCELED.
Troubleshooting
- The new command did not run: Check for pending changes. Apply them, or pass a per-run
jobConfigoverride. - Job command is required: Set
jobConfig.commandon the app or environment, apply it, or passjobConfig.commandwhen creating the run. - Job image is not ready: Deploy an image or complete a build before running the Job.
- Concurrency limit reached: Wait for the active run to finish, set
maxConcurrency, or enableallowConcurrentRuns. - No logs yet: A run may still be
QUEUEDorSTARTING. Fetch the run status first, then request logs after it reachesRUNNINGor a terminal status.
For the broader product taxonomy, see App Types. For endpoint-level details, see the Job Runs API.