Scheduled Jobs
Run your app's work on a schedule.
A scheduled job calls one of the app's own routes (for example POST /api/cron/daily-report) on a schedule. A job belongs to one environment: a dev job calls the development preview, a live job calls the published site. Create dev jobs while building: publishing creates their live twins and every later publish carries the dev job's changes (schedule, address, time zone, on/off) to its twin and deletes the twin of a deleted dev job. live jobs you create yourself are never touched by a publish. The app can also create jobs itself with totalum.cron (SDK).
| Plan | Jobs per environment | Shortest interval |
|---|---|---|
| Free | 2 | 1 hour |
| Starter | 10 | 1 minute |
| Builder, Professional, Enterprise | 20 | 1 minute |
Cost. Creating and keeping jobs is free. Each run costs about 0.0004 credits for the scheduling itself, plus what the request costs like any visit (its compute time, its database rows, any built-in feature it calls). A light job every hour costs about 0.36 credits a month; every minute, about 18.6. A failed run (an answer that is not 2xx, or no answer in time) counts toward pauseAfterFailures; the owner is told by e-mail and the cron.job.paused / cron.job.failing webhooks.
#List Cron Jobs
/api/v1/vcaas/projects/:projectId/cron-jobsapi-keyFreeThe project's jobs, both environments.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.items | object[] | The jobs |
data.items[].id | string | Job id |
data.items[].name | string | Name |
data.items[].schedule | string | object | A 5-field cron expression, or { everyMinutes } |
data.items[].timezone | string | undefined | IANA time zone of the schedule (absent = UTC) |
data.items[].request | object | { method, url, headers?, body?, timeoutMs? } |
data.items[].env | string | "dev" or "live" |
data.items[].enabled | boolean | Switched on |
data.items[].status | string | "active", "paused" or "paused_failures" (paused after too many failures) |
data.items[].nextRunAt | string | null | Next run |
data.items[].lastRun | object | undefined | { at, status, latencyMs }; status is the HTTP status, or "timeout", "error", "skipped" |
data.items[].pauseAfterFailures | number | null | Failures in a row that pause it; null = never |
data.items[].description | string | undefined | Description |
data.items[].createdAt | string | ISO 8601 |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/cron-jobsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/cron-jobs", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "items": [{ "id": "cj_01k6xv", "name": "daily-report", "schedule": "0 9 * * *", "timezone": "Europe/Madrid", "request": { "method": "POST", "url": "https://barber-bookings.totalum-project.com/api/cron/daily-report" }, "env": "live", "enabled": true, "status": "active", "nextRunAt": "2026-10-09T07:00:00.000Z", "pauseAfterFailures": 3, "createdAt": "2026-10-08T12:00:00.000Z" }] } }#Create Cron Job
/api/v1/vcaas/projects/:projectId/cron-jobsapi-keyFreeCreate a job. The URL must be on the environment's own host: the preview address for dev, the published <projectId>.totalum-project.com for live (which needs a published project). Without env the job belongs to the environment of its URL: the preview address makes a dev job (so an unpublished project needs no env), any other address a live one.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | 1–64 characters |
schedule | string | object | Yes | A 5-field cron expression ("*/15 * * * *"), or { "everyMinutes": 30 } |
request | object | Yes | What to call |
request.method | string | Yes | "GET" or "POST" |
request.url | string | Yes | https:// URL on the environment's host |
request.headers | object | No | Up to 10 headers; a value may be {{secret:NAME}} to send a project secret |
request.body | string | No | Up to 64 KB |
request.timeoutMs | number | No | Up to 30,000 |
env | string | No | "dev" or "live" (default: the environment of request.url — "dev" for the preview address, else "live") |
timezone | string | No | IANA zone, e.g. "Europe/Madrid": "0 9 * * *" runs at 09:00 there all year. Default UTC |
pauseAfterFailures | number | null | No | 1–100 failures in a row pause the job (default 3); null or 0 = never pause |
enabled | boolean | No | Default true |
description | string | No | Free text |
Response fields
| Field | Type | Description |
|---|---|---|
data | object | The job, with the fields of items[] in List Cron Jobs |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"daily-report","env":"dev","schedule":"0 9 * * *","timezone":"Europe/Madrid","request":{"method":"POST","url":"https://barber-bookings-dev-temporal-link-preview-k3y9x2ab.totalum-project.com/api/cron/daily-report"}}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/cron-jobsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/cron-jobs", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "daily-report",
"env": "dev",
"schedule": "0 9 * * *",
"timezone": "Europe/Madrid",
"request": {
"method": "POST",
"url": "https://barber-bookings-dev-temporal-link-preview-k3y9x2ab.totalum-project.com/api/cron/daily-report"
}
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "id": "cj_01k6xw", "name": "daily-report", "schedule": "0 9 * * *", "timezone": "Europe/Madrid", "request": { "method": "POST", "url": "https://barber-bookings-dev-temporal-link-preview-k3y9x2ab.totalum-project.com/api/cron/daily-report" }, "env": "dev", "enabled": true, "status": "active", "nextRunAt": "2026-10-09T07:00:00.000Z", "pauseAfterFailures": 3, "createdAt": "2026-10-08T12:00:00.000Z" } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
INVALID_SCHEDULE | 400 | Invalid, or more frequent than your plan allows (errorDetails.minIntervalSeconds) |
VALIDATION_ERROR | 400 | A field is invalid (an unknown timezone, pauseAfterFailures out of range…) |
CRON_URL_NOT_ALLOWED | 403 | The URL is not on the environment's host (errorDetails.allowedHost) |
CRON_REQUIRES_PUBLISHED_PROJECT | 403 | A live job on a project that is not published |
CRON_LIMIT_REACHED | 429 | The environment already has your plan's number of jobs (errorDetails.limit) |
#Get Cron Job
/api/v1/vcaas/projects/:projectId/cron-jobs/:jobIdapi-keyFreeOne job.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
jobId | string | The job id |
Response fields
| Field | Type | Description |
|---|---|---|
data | object | The job, as in List Cron Jobs |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/cron-jobs/cj_01k6xwconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/cron-jobs/cj_01k6xw", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "id": "cj_01k6xw", "name": "daily-report", "schedule": "0 9 * * *", "request": { "method": "POST", "url": "https://barber-bookings-dev-temporal-link-preview-k3y9x2ab.totalum-project.com/api/cron/daily-report" }, "env": "dev", "enabled": true, "status": "active", "nextRunAt": "2026-10-09T09:00:00.000Z", "pauseAfterFailures": 3, "createdAt": "2026-10-08T12:00:00.000Z" } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
CRON_JOB_NOT_FOUND | 404 | No such job |
#Update Cron Job
/api/v1/vcaas/projects/:projectId/cron-jobs/:jobIdapi-keyFreeChange any field of a job, or switch it on or off (enabled). A paused job runs again once it is enabled.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
jobId | string | The job id |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
name, schedule, request, env, timezone, pauseAfterFailures, enabled, description | No | Any field of Create Cron Job |
Response fields
| Field | Type | Description |
|---|---|---|
data | object | The job after the change |
Example request
curl -X PATCH \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"enabled":false}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/cron-jobs/cj_01k6xwconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/cron-jobs/cj_01k6xw", {
method: "PATCH",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"enabled": false
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "id": "cj_01k6xw", "name": "daily-report", "schedule": "0 9 * * *", "request": { "method": "POST", "url": "https://barber-bookings-dev-temporal-link-preview-k3y9x2ab.totalum-project.com/api/cron/daily-report" }, "env": "dev", "enabled": false, "status": "paused", "nextRunAt": null, "pauseAfterFailures": 3, "createdAt": "2026-10-08T12:00:00.000Z" } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
CRON_JOB_NOT_FOUND | 404 | No such job |
INVALID_SCHEDULE | 400 | Invalid or too frequent schedule |
CRON_URL_NOT_ALLOWED | 403 | The URL is not on the environment's host |
#Delete Cron Job
/api/v1/vcaas/projects/:projectId/cron-jobs/:jobIdapi-keyFreeDelete a job.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
jobId | string | The job id |
Response fields
| Field | Type | Description |
|---|---|---|
data.deleted | boolean | true |
Example request
curl -X DELETE -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/cron-jobs/cj_01k6xwconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/cron-jobs/cj_01k6xw", {
method: "DELETE",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "deleted": true } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
CRON_JOB_NOT_FOUND | 404 | No such job |
#Run Cron Job
/api/v1/vcaas/projects/:projectId/cron-jobs/:jobId/runapi-keyFreeRun a job now, once. The run appears in its executions. At most 10 manual runs a minute per project.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
jobId | string | The job id |
Response fields
| Field | Type | Description |
|---|---|---|
data.runId | string | The execution |
Example request
curl -X POST -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/cron-jobs/cj_01k6xw/runconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/cron-jobs/cj_01k6xw/run", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "runId": "cr_01k6xx" } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
CRON_JOB_NOT_FOUND | 404 | No such job |
RATE_LIMITED | 429 | More than 10 manual runs in a minute; retry after errorDetails.retryAfterSeconds |
#List Cron Job Executions
/api/v1/vcaas/projects/:projectId/cron-jobs/:jobId/executionsapi-keyFreeA job's executions of the last 30 days, newest first.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
jobId | string | The job id |
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
limit | number | No | Page size |
cursor | string | No | nextCursor of the previous page |
Response fields
| Field | Type | Description |
|---|---|---|
data.items[].id | string | Execution id |
data.items[].scheduledFor | string | When it was due |
data.items[].startedAt | string | null | When it ran |
data.items[].status | number | string | HTTP status, or "timeout", "error", "skipped" (not run: no credits, or the cron feature is off or over budget) |
data.items[].latencyMs | number | null | Response time |
data.items[].error | string | undefined | Why it failed or was skipped |
data.items[].responseSnippet | string | undefined | The first 1 KB of the answer |
data.nextCursor | string | null | Next page |
Example request
curl -H "api-key: tlm_sk_your_key" \
"https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/cron-jobs/cj_01k6xw/executions?limit=20"const res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/cron-jobs/cj_01k6xw/executions?limit=20", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "items": [{ "id": "cr_01k6xx", "scheduledFor": "2026-10-08T12:05:00.000Z", "startedAt": "2026-10-08T12:05:00.000Z", "status": 200, "latencyMs": 142, "responseSnippet": "{\"ok\":true}" }], "nextCursor": null } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
CRON_JOB_NOT_FOUND | 404 | No such job |
