# Totalum App Builder API — Scheduled Jobs

This file is the complete reference for this area. The short guide for agents is https://www.totalum.app/totalum-api.md.

- Base URL: `https://builder-api.totalum.app/api/v1/vcaas`
- Authentication: header `api-key: tlm_sk_…` on every request, from a server only.
- Response envelope: `{ "errors": null | { "errorCode", "errorMessage", "errorDetails?" }, "data": … }`.


---

# 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](https://www.totalum.app/docs/sdk/cron)).

| 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](https://www.totalum.app/docs/api/webhooks).

## List Cron Jobs

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/cron-jobs
cost: Free
```

The 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**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/cron-jobs
```

:::success Success · 200 OK
```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

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/cron-jobs
cost: Free
```

Create 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](#list-cron-jobs) |

**Example request**

```bash
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-jobs
```

:::success Success · 201 Created
```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

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/cron-jobs/:jobId
cost: Free
```

One 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](#list-cron-jobs) |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/cron-jobs/cj_01k6xw
```

:::success Success · 200 OK
```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

```endpoint
method: PATCH
path: /api/v1/vcaas/projects/:projectId/cron-jobs/:jobId
cost: Free
```

Change 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](#create-cron-job) |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data` | object | The job after the change |

**Example request**

```bash
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_01k6xw
```

:::success Success · 200 OK
```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

```endpoint
method: DELETE
path: /api/v1/vcaas/projects/:projectId/cron-jobs/:jobId
cost: Free
```

Delete 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**

```bash
curl -X DELETE -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/cron-jobs/cj_01k6xw
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "deleted": true } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `CRON_JOB_NOT_FOUND` | 404 | No such job |

## Run Cron Job

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/cron-jobs/:jobId/run
cost: Free
```

Run 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**

```bash
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/run
```

:::success Success · 200 OK
```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

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/cron-jobs/:jobId/executions
cost: Free
```

A 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**

```bash
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"
```

:::success Success · 200 OK
```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 |
