Skip to main content

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:migrate or bundle 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

  1. Open the account in Quave ONE.
  2. Go to Apps.
  3. In the Jobs section, click Add new Job.
  4. Choose the deployment source, usually Use image for a pre-built image or CLI deployment for source builds.
  5. Set the Job app name, image or build configuration, region, and environment.
  6. Open App Config and configure Job Defaults.
  7. Open the environment and select Settings → Job Config if that environment needs a command or timeout override.
  8. 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:

ToolPurpose
create-appCreate the Job app with dockerPreset: "JOB".
create-app-envCreate the environment for the Job app.
deploy-app-env-imageDeploy a pre-built image to the Job environment.
update-app-env-job-configEdit environment-level Job execution settings.
get-app-env-job-scheduleInspect the environment schedule, next run, and recent occurrences.
update-app-env-job-scheduleConfigure, enable, or disable one recurring schedule immediately.
apply-app-env-changesApply pending Job settings to the environment.
run-app-env-jobOptionally merge environment variables and create a new JobRun; can wait briefly for a concurrency slot with waitForConcurrencySlot.
list-app-env-job-runsList previous runs with page, status, and time filters.
get-app-env-job-runCheck one run.
get-app-env-job-run-logsFetch logs for one run.
cancel-app-env-job-runCancel an active run.
rerun-app-env-job-runRerun 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:

LocationWhat it controlsSave behavior
App Config → Job DefaultsDefault command and execution settings inherited by every environment.Creates pending changes for the Job environments.
Environment → Settings → Job ConfigCommand and execution overrides for one environment. This is the first Settings subtab for Jobs.Creates pending changes for that environment.
Environment → Settings → ScheduleOne 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 InstructionsEnvironment 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.

FieldDescription
commandCommand to execute. Required before a run starts unless each run provides an override.
argsOptional argument list. If shell is true, arguments can also be included directly in command.
shellRuns the command through /bin/sh -lc when true. Default: true.
workingDirOptional working directory inside the container.
timeoutSecondsActive deadline for a run. Default: 1800 seconds.
ttlSecondsAfterFinishedKubernetes TTL after completion. Default: 21600 seconds. Minimum: 300 seconds.
backoffLimitKubernetes Job retry backoff limit. Default: 0.
maxConcurrencyMaximum active runs for this Job environment.
allowConcurrentRunsAllows 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 @daily are rejected.
  • Use an IANA time zone such as UTC, America/Sao_Paulo, or Europe/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:

  1. App defaults: Set on the Job app as jobConfig.
  2. Environment overrides: Set on one app environment as jobConfig.
  3. Applied deployment snapshot: Captured from the app and environment settings when the environment image/build is deployed or when pending Job settings are applied.
  4. 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 tool apply-app-env-changes, or save with applyImmediately: true.
  • Passing a per-run jobConfig to run-app-env-job or POST /api/public/v1/app-env/job-runs overrides the applied snapshot for that run only.
  • Historical JobRun records 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:

  1. Build or publish the same image version that your app should migrate.
  2. Create a Job app with dockerPreset: "JOB" and that image.
  3. Set the app default command to the Rails command that exists inside your container, usually bin/rails db:migrate for apps with Rails binstubs or bundle exec rails db:migrate.
  4. Create one environment per target, for example staging and production.
  5. Deploy the image to the target Job environment.
  6. Apply pending Job setting changes if you edited the command after the deploy.
  7. Run the Job.
  8. Poll the JobRun until it is terminal.
  9. 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:

  1. Deploy or register the new image on the migration Job environment.
  2. Create the JobRun and save the returned jobRunId.
  3. Poll GET /api/public/v1/app-env/job-run?jobRunId=JOB_RUN_ID until isTerminal is true.
  4. Continue only when status is SUCCEEDED or isSuccess is true.
  5. If the status is FAILED, CANCELED, TIMED_OUT, or NOT_FOUND, fetch logs and stop the app deploy.
  6. 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 contentId or provide a new per-run jobConfig.
  • Cancel deletes the Kubernetes Job for an active run and marks the JobRun as CANCELED.

Troubleshooting

  • The new command did not run: Check for pending changes. Apply them, or pass a per-run jobConfig override.
  • Job command is required: Set jobConfig.command on the app or environment, apply it, or pass jobConfig.command when 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 enable allowConcurrentRuns.
  • No logs yet: A run may still be QUEUED or STARTING. Fetch the run status first, then request logs after it reaches RUNNING or a terminal status.

For the broader product taxonomy, see App Types. For endpoint-level details, see the Job Runs API.