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"
}
statusisactiveorpaused. A paused job's schedule starts nothing.environmentIdis a saved environment's id (its image and size), orcustom, whereresourcesgives the size and the platform's runtime image is used.environmentVersionpins a version of the environment; without it a run uses its latest.scheduleis{ "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" }.nextRunAtis the next scheduled run, once the scheduler has computed it.lastRunis 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"
}
statusispending,running,succeeded,failedorcancelled.triggerismanualorscheduled.userIdis who the run ran as: who ran it, or the job's creator for a scheduled run.errorsays why a run failed, or why it was refused (by governance or a budget; a refused run has nok8sJobName).logsis 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
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | No | Only this project's jobs |
status | string | No | active or paused |
q | string | No | Case-insensitive match on name or description |
limit | integer | No | Number of results to return (default: 50, max: 200) |
cursor | string | No | meta.nextCursor of the previous page; omit for the first page |
sort | string | No | Sort 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" }
}
| Field | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | The project the job belongs to |
name | string | Yes | Job name |
command | string | Yes | The command line each run runs with bash in the project's code folder |
workspaceVolumeSize | string | Yes | Each run's own working storage, in whole GB (e.g. "20GB") |
sharedVolumeIds | string[] | No | The 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). |
addons | string[] | Yes | Add-on ids whose connections the runs get ([] for none) |
dataSources | string[] | Yes | Data source ids ([] for none) |
aiModels | string[] | Yes | AI Gateway model ids ([] for none) |
workflows | string[] | Yes | Workflow ids ([] for none) |
description | string | No | What the job does |
environmentId | string | No | A saved environment's id; without one, resources is required |
environmentVersion | integer | No | A version of that environment to pin (default: its latest) |
resources | object | No | The size of each run without a saved environment: cpu and memory (required then), disk, gpu, gpu_type. No size is assumed |
useSpot | boolean | No | Run each run on spot capacity (cheaper; a reclaimed run is interrupted). Default false |
spotFallback | boolean | No | With useSpot, fall back to on-demand capacity when no spot is available (default true); false waits for spot |
envVars | object | No | Environment variables set in each run |
schedule | object | No | When 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
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | integer | No | Number of results to return (default: 50, max: 200) |
cursor | string | No | meta.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
| Parameter | Type | Required | Description |
|---|---|---|---|
download | boolean | No | true: 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>" }.