Skip to main content

Jobs

A job runs a command line from a project, on demand or on a schedule. Each run runs once, in its own environment at the job's size, and mounts the project's volume (the command runs in its code folder, at /volumes/local/<volume name>) and the shared volumes chosen for the job (sharedVolumeIds) at /volumes/shared/<name>, as a workspace does. A job never runs twice at once: a scheduled slot while a run is still going starts nothing, and a run requested then is refused with why.

A run's STRONGLY_SERVICES environment variable holds the connections of the services the job selects (add-ons, data sources, AI models and workflows), built the same way as a deployed app's.

All endpoints require authentication via X-API-Key header and the appropriate scope.


Job Object​

{
"_id": "job-a1b2c3d4e5f6",
"projectId": "project-8kFq2wLx",
"organizationId": "org_xyz",
"name": "Nightly training",
"description": "Retrains the churn model",
"command": "python3 train.py --epochs 3",
"environment": "development",
"environmentId": "custom",
"resources": { "cpu": "1", "memory": "4GB" },
"workspaceVolumeSize": "20GB",
"useSpot": false,
"spotFallback": true,
"envVars": { "EPOCHS": "3" },
"addons": [],
"dataSources": ["ds_123"],
"aiModels": [],
"workflows": [],
"schedule": { "type": "cron", "cron": "0 2 * * *", "timeZone": "America/New_York" },
"status": "active",
"nextRunAt": "2026-09-30T06:00:00.000Z",
"lastRun": {
"executionId": "YqZ3p9kLm2",
"status": "succeeded",
"startedAt": "2026-09-29T06:00:04.000Z",
"completedAt": "2026-09-29T06:03:11.000Z"
},
"createdBy": "user_456",
"createdAt": "2026-09-01T10:00:00.000Z",
"updatedAt": "2026-09-20T12:00:00.000Z"
}
  • status is active or paused. A paused job's schedule starts nothing.
  • environmentId is a saved environment's id (its image and size), or custom, where resources gives the size and the platform's runtime image is used. environmentVersion pins a version of the environment; without it a run uses its latest.
  • schedule is { "type": "manual" } (on demand only), { "type": "cron", "cron", "timeZone" } (a standard 5-field expression whose times are local times in the IANA zone, kept across daylight saving changes), or { "type": "once", "runAt" }.
  • nextRunAt is the next scheduled run, once the scheduler has computed it.
  • lastRun is the job's latest run.

Execution Object​

{
"_id": "YqZ3p9kLm2",
"jobId": "job-a1b2c3d4e5f6",
"projectId": "project-8kFq2wLx",
"organizationId": "org_xyz",
"status": "succeeded",
"trigger": "scheduled",
"userId": "user_456",
"command": "python3 train.py --epochs 3",
"startedAt": "2026-09-29T06:00:04.000Z",
"completedAt": "2026-09-29T06:03:11.000Z",
"duration": 187,
"exitCode": 0,
"logs": "epoch 3/3 done\n",
"k8sJobName": "job-a1b2c3d4e5f6-yqz3p9",
"environment": "development"
}
  • status is pending, running, succeeded, failed or cancelled.
  • trigger is manual or scheduled. userId is who the run ran as: who ran it, or the job's creator for a scheduled run.
  • error says why a run failed, or why it was refused (by governance or a budget; a refused run has no k8sJobName).
  • logs is the log's tail. The whole log is kept separately.

GET /api/v1/jobs​

List the jobs you can see: across all your projects, or one project's.

Scope: jobs:read

Query Parameters

ParameterTypeRequiredDescription
projectIdstringNoOnly this project's jobs
statusstringNoactive or paused
qstringNoCase-insensitive match on name or description
limitintegerNoNumber of results to return (default: 50, max: 200)
cursorstringNometa.nextCursor of the previous page; omit for the first page
sortstringNoSort field, prefixed with - for descending (default: -createdAt)

Response 200 OK

{
"data": [ { "_id": "job-a1b2c3d4e5f6", "name": "Nightly training", "status": "active" } ],
"meta": { "total": 1, "limit": 50, "nextCursor": null, "requestId": "req_abc123" }
}

Each item is a job.


POST /api/v1/jobs​

Create a job in a project.

Scope: jobs:write

Request Body

{
"projectId": "project-8kFq2wLx",
"name": "Nightly training",
"command": "python3 train.py --epochs 3",
"workspaceVolumeSize": "20GB",
"resources": { "cpu": "1", "memory": "4GB" },
"addons": [],
"dataSources": ["ds_123"],
"aiModels": [],
"workflows": [],
"schedule": { "type": "cron", "cron": "0 2 * * *", "timeZone": "America/New_York" }
}
FieldTypeRequiredDescription
projectIdstringYesThe project the job belongs to
namestringYesJob name
commandstringYesThe command line each run runs with bash in the project's code folder
workspaceVolumeSizestringYesEach run's own working storage, in whole GB (e.g. "20GB")
sharedVolumeIdsstring[]NoThe shared volumes each run mounts at /volumes/shared/<name> (code/ read-only). None unless listed; the project's volume always mounts. A volume the job's creator may not mount is refused with 400, by name, and one whose share is later taken away is dropped from the list. Change it with PUT (from the next run).
addonsstring[]YesAdd-on ids whose connections the runs get ([] for none)
dataSourcesstring[]YesData source ids ([] for none)
aiModelsstring[]YesAI Gateway model ids ([] for none)
workflowsstring[]YesWorkflow ids ([] for none)
descriptionstringNoWhat the job does
environmentIdstringNoA saved environment's id; without one, resources is required
environmentVersionintegerNoA version of that environment to pin (default: its latest)
resourcesobjectNoThe size of each run without a saved environment: cpu and memory (required then), disk, gpu, gpu_type. No size is assumed
useSpotbooleanNoRun each run on spot capacity (cheaper; a reclaimed run is interrupted). Default false
spotFallbackbooleanNoWith useSpot, fall back to on-demand capacity when no spot is available (default true); false waits for spot
envVarsobjectNoEnvironment variables set in each run
scheduleobjectNoWhen the job runs by itself (see the job object)

Returns 400 validation-error when a required field is missing, the command is empty, the volume size is not whole GB, or no size is given without a saved environment.

Response 201 Created

The new job.


GET /api/v1/jobs/:id​

Get a job.

Scope: jobs:read

Response 200 OK: data is the job. 404 not-found for a job you cannot see.


PATCH /api/v1/jobs/:id​

Change a job. Only the fields given change: name, description, command, environmentId, environmentVersion, resources, useSpot, spotFallback, workspaceVolumeSize, envVars, schedule, addons, dataSources, aiModels, mlModels, workflows, featureStores and agents (each list replaces the job's). A size below the job minimum (0.3 CPU and 768 MB: the platform's containers beside the command take their share) is refused when it runs. A new schedule applies from its next run. Switching to another environment without a version runs that environment's latest. Pause and resume a job with the routes below.

Scope: jobs:write

Response 200 OK

The updated job.


DELETE /api/v1/jobs/:id​

Delete a job. Its live runs are cancelled.

Scope: jobs:write

Response 204 No Content


POST /api/v1/jobs/:id/run​

Start a run now, as you.

Scope: jobs:write

Response 200 OK

{ "data": { "executionId": "YqZ3p9kLm2", "k8sJobName": "job-a1b2c3d4e5f6-yqz3p9" }, "meta": { "requestId": "req_abc123" } }

A run refused because the job's last run is still going, or by governance or a budget, answers with the reason; a refusal by governance or a budget is also recorded in the run history, with the reason in its error.


POST /api/v1/jobs/:id/pause​

Pause a job: its schedule starts nothing until it is resumed.

Scope: jobs:write

Response 200 OK: the job, as GET /jobs/:id shows it.


POST /api/v1/jobs/:id/resume​

Resume a paused job.

Scope: jobs:write

Response 200 OK: the job, as GET /jobs/:id shows it.


GET /api/v1/jobs/:id/executions​

List a job's runs, newest first.

Scope: jobs:read

Query Parameters

ParameterTypeRequiredDescription
limitintegerNoNumber of results to return (default: 50, max: 200)
cursorstringNometa.nextCursor of the previous page; omit for the first page

Response 200 OK: a page of executions, with meta as in the job list.


GET /api/v1/jobs/:jobId/executions/:id​

Get a run: its status, trigger, user, command, exit code, error and log tail.

Scope: jobs:read

Response 200 OK: data is the execution.


GET /api/v1/jobs/:jobId/executions/:id/log-url​

Get a URL to a run's whole log, valid for 5 minutes: a finished run's, or all a running run has written so far.

Scope: jobs:read

Query Parameters

ParameterTypeRequiredDescription
downloadbooleanNotrue: the URL downloads the log as a file

Response 200 OK

{ "data": { "url": "https://..." }, "meta": { "requestId": "req_abc123" } }

POST /api/v1/jobs/:jobId/executions/:id/cancel​

Cancel a run: it is stopped and recorded cancelled.

Scope: jobs:write

Response 200 OK: the execution, cancelled (or as it ended, when it had already finished).

A run that has already ended answers { "cancelled": false, "status": "<its status>" }.