# Cron and jobs

> Schedules that call a path of an app or run a command on a machine of their own, their run history, and one-off jobs.

A schedule runs something of an app on a cron expression: either it wakes the app and calls a path (signed, so the app can verify the call), or it runs a command on a machine of its own, sized as you say. A job is the same machine without a schedule: one command, run once, with its exit code and output kept. See the guide [Cron and jobs](https://docs.rytiv.com/guides/cron-and-jobs.md).

## Endpoints

- [`POST /v1/apps/:name/schedules`](#post-v1appsnameschedules): Adds a schedule: a cron expression and either a `path` the app is woken for, or a `command` run on a machine of its own
- [`GET /v1/apps/:name/schedules`](#get-v1appsnameschedules): The app's schedules with their next slot and last run
- [`GET /v1/apps/:name/schedules/:id`](#get-v1appsnameschedulesid): One schedule
- [`PATCH /v1/apps/:name/schedules/:id`](#patch-v1appsnameschedulesid): Changes a schedule. The kind of target (path or command) stays
- [`DELETE /v1/apps/:name/schedules/:id`](#delete-v1appsnameschedulesid): Removes the schedule and its run history
- [`GET /v1/apps/:name/schedules/:id/runs`](#get-v1appsnameschedulesidruns): The schedule's runs, newest first
- [`POST /v1/apps/:name/schedules/:id/runs`](#post-v1appsnameschedulesidruns): Runs the schedule now, outside its expression
- [`POST /v1/apps/:name/jobs`](#post-v1appsnamejobs): Runs one command on a machine of its own with the app's image and environment. Answers `202` with the job
- [`GET /v1/apps/:name/jobs`](#get-v1appsnamejobs): The app's jobs
- [`GET /v1/jobs/:id`](#get-v1jobsid): One job with its state, exit code and output
- [`DELETE /v1/jobs/:id`](#delete-v1jobsid): Cancels a job that still runs

## POST /v1/apps/:name/schedules

Adds a schedule: a cron expression and either a `path` the app is woken for, or a `command` run on a machine of its own.

**Auth:** API key with full access (a key with scopes cannot call this route). An agent token of class `sandbox` or `twin` may call it; class `production` is refused (`agent-token-production-readonly`).

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The app's name. |

### Request body

JSON, the shape `ScheduleCreate` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `schedule` | string | required | A cron expression with five fields, or `@hourly`, `@daily` and the like. 1 to 200 characters. |
| `timezone` | string | optional | The IANA time zone the expression is read in. 1 to 64 characters, default `"UTC"`. |
| `path` | string | optional | The app is woken and this path is called. 1 to 2048 characters. |
| `method` | `GET` \| `POST` | optional | With `path`: `GET` (default) or `POST`. |
| `command` | array of string | optional | Runs on a machine of its own, as an argument list. 1 to 64 items. |
| `timeoutSec` | integer | optional | The deadline in seconds: per attempt for a path, for the whole run for a command. 1 to 9007199254740991. |
| `memoryMb` | integer | optional | Memory in MB. -9007199254740991 to 9007199254740991. |
| `vcpus` | integer | optional | Virtual CPUs. -9007199254740991 to 9007199254740991. |
| `enabled` | boolean | optional | `false` creates the schedule paused. default `true`. |

### Example request

```bash
curl -X POST "https://api.rytiv.app/v1/apps/<name>/schedules" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"schedule":"*/15 * * * *"}'
```

### Response

`201` with the schedule:

```json
{
  "id": "<id>",
  "app": "<app>",
  "schedule": "*/15 * * * *",
  "timezone": "Europe/Berlin",
  "timeoutSec": -9007199254740991,
  "enabled": true,
  "createdAt": "2026-10-21T09:00:00Z",
  "updatedAt": "2026-10-21T09:00:00Z",
  "secretCreated": true
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `id` | string | always |  |
| `app` | string | always |  |
| `schedule` | string | always | A cron expression with five fields, or `@hourly`, `@daily` and the like. |
| `timezone` | string | always | The IANA time zone the expression is read in. |
| `path` | string | if set | The app is woken and this path is called. |
| `method` | `GET` \| `POST` | if set | With `path`: `GET` (default) or `POST`. |
| `command` | array of string | if set | Runs on a machine of its own, as an argument list. |
| `timeoutSec` | integer | always | The deadline in seconds: per attempt for a path, for the whole run for a command. -9007199254740991 to 9007199254740991. |
| `memoryMb` | integer | if set | Memory in MB. -9007199254740991 to 9007199254740991. |
| `vcpus` | integer | if set | Virtual CPUs. -9007199254740991 to 9007199254740991. |
| `enabled` | boolean | always | `false` creates the schedule paused. |
| `nextRunAt` | string (ISO 8601) | if set |  |
| `lastRun` | object | if set |  |
| `lastRun.id` | string | always |  |
| `lastRun.schedule` | string | always | A cron expression with five fields, or `@hourly`, `@daily` and the like. |
| `lastRun.app` | string | always |  |
| `lastRun.scheduledFor` | string (ISO 8601) | always |  |
| `lastRun.trigger` | `schedule` \| `manual` | always |  |
| `lastRun.state` | `running` \| `succeeded` \| `failed` \| `skipped` \| `missed` | always |  |
| `lastRun.attempts` | array of object | always |  |
| `lastRun.statusCode` | integer | if set | -9007199254740991 to 9007199254740991. |
| `lastRun.error` | string | if set |  |
| `lastRun.job` | string | if set |  |
| `lastRun.exitCode` | integer | if set | -9007199254740991 to 9007199254740991. |
| `lastRun.missedSlots` | integer | always | 0 to 9007199254740991. |
| `lastRun.startedAt` | string (ISO 8601) | if set |  |
| `lastRun.finishedAt` | string (ISO 8601) | if set |  |
| `lastRun.durationMs` | integer | if set | 0 to 9007199254740991. |
| `createdAt` | string (ISO 8601) | always | When it was created. |
| `updatedAt` | string (ISO 8601) | always |  |
| `secretCreated` | boolean | always |  |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `bad-request` | The request is malformed or a field is out of range. `error` names every field; `issues` lists them. |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |
| 410 | `gone` | The object existed and is gone. |

## GET /v1/apps/:name/schedules

The app's schedules with their next slot and last run.

**Auth:** API key with full access or the scope `read`. Every class of agent token may call it.

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The app's name. |

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/apps/<name>/schedules" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with a list of schedules, each:

```json
[
  {
    "id": "<id>",
    "app": "<app>",
    "schedule": "*/15 * * * *",
    "timezone": "Europe/Berlin",
    "timeoutSec": -9007199254740991,
    "enabled": true,
    "createdAt": "2026-10-21T09:00:00Z",
    "updatedAt": "2026-10-21T09:00:00Z"
  }
]
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `id` | string | always | The schedule's id. |
| `app` | string | always |  |
| `schedule` | string | always | A cron expression with five fields, or `@hourly`, `@daily` and the like. |
| `timezone` | string | always | The IANA time zone the expression is read in. |
| `path` | string | if set | The app is woken and this path is called. |
| `method` | `GET` \| `POST` | if set | With `path`: `GET` (default) or `POST`. |
| `command` | array of string | if set | Runs on a machine of its own, as an argument list. |
| `timeoutSec` | integer | always | The deadline in seconds: per attempt for a path, for the whole run for a command. -9007199254740991 to 9007199254740991. |
| `memoryMb` | integer | if set | Memory in MB. -9007199254740991 to 9007199254740991. |
| `vcpus` | integer | if set | Virtual CPUs. -9007199254740991 to 9007199254740991. |
| `enabled` | boolean | always | `false` creates the schedule paused. |
| `nextRunAt` | string (ISO 8601) | if set | The next slot. |
| `lastRun` | object | if set | The last run, if any. |
| `lastRun.id` | string | always |  |
| `lastRun.schedule` | string | always | A cron expression with five fields, or `@hourly`, `@daily` and the like. |
| `lastRun.app` | string | always |  |
| `lastRun.scheduledFor` | string (ISO 8601) | always |  |
| `lastRun.trigger` | `schedule` \| `manual` | always |  |
| `lastRun.state` | `running` \| `succeeded` \| `failed` \| `skipped` \| `missed` | always |  |
| `lastRun.attempts` | array of object | always |  |
| `lastRun.statusCode` | integer | if set | -9007199254740991 to 9007199254740991. |
| `lastRun.error` | string | if set |  |
| `lastRun.job` | string | if set |  |
| `lastRun.exitCode` | integer | if set | -9007199254740991 to 9007199254740991. |
| `lastRun.missedSlots` | integer | always | 0 to 9007199254740991. |
| `lastRun.startedAt` | string (ISO 8601) | if set |  |
| `lastRun.finishedAt` | string (ISO 8601) | if set |  |
| `lastRun.durationMs` | integer | if set | 0 to 9007199254740991. |
| `createdAt` | string (ISO 8601) | always | When it was created. |
| `updatedAt` | string (ISO 8601) | always |  |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## GET /v1/apps/:name/schedules/:id

One schedule.

**Auth:** API key with full access or the scope `read`. Every class of agent token may call it.

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The app's name. |
| `id` | The object's id. |

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/apps/<name>/schedules/<id>" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with the schedule:

```json
{
  "id": "<id>",
  "app": "<app>",
  "schedule": "*/15 * * * *",
  "timezone": "Europe/Berlin",
  "timeoutSec": -9007199254740991,
  "enabled": true,
  "createdAt": "2026-10-21T09:00:00Z",
  "updatedAt": "2026-10-21T09:00:00Z"
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `id` | string | always | The schedule's id. |
| `app` | string | always |  |
| `schedule` | string | always | A cron expression with five fields, or `@hourly`, `@daily` and the like. |
| `timezone` | string | always | The IANA time zone the expression is read in. |
| `path` | string | if set | The app is woken and this path is called. |
| `method` | `GET` \| `POST` | if set | With `path`: `GET` (default) or `POST`. |
| `command` | array of string | if set | Runs on a machine of its own, as an argument list. |
| `timeoutSec` | integer | always | The deadline in seconds: per attempt for a path, for the whole run for a command. -9007199254740991 to 9007199254740991. |
| `memoryMb` | integer | if set | Memory in MB. -9007199254740991 to 9007199254740991. |
| `vcpus` | integer | if set | Virtual CPUs. -9007199254740991 to 9007199254740991. |
| `enabled` | boolean | always | `false` creates the schedule paused. |
| `nextRunAt` | string (ISO 8601) | if set | The next slot. |
| `lastRun` | object | if set | The last run, if any. |
| `lastRun.id` | string | always |  |
| `lastRun.schedule` | string | always | A cron expression with five fields, or `@hourly`, `@daily` and the like. |
| `lastRun.app` | string | always |  |
| `lastRun.scheduledFor` | string (ISO 8601) | always |  |
| `lastRun.trigger` | `schedule` \| `manual` | always |  |
| `lastRun.state` | `running` \| `succeeded` \| `failed` \| `skipped` \| `missed` | always |  |
| `lastRun.attempts` | array of object | always |  |
| `lastRun.statusCode` | integer | if set | -9007199254740991 to 9007199254740991. |
| `lastRun.error` | string | if set |  |
| `lastRun.job` | string | if set |  |
| `lastRun.exitCode` | integer | if set | -9007199254740991 to 9007199254740991. |
| `lastRun.missedSlots` | integer | always | 0 to 9007199254740991. |
| `lastRun.startedAt` | string (ISO 8601) | if set |  |
| `lastRun.finishedAt` | string (ISO 8601) | if set |  |
| `lastRun.durationMs` | integer | if set | 0 to 9007199254740991. |
| `createdAt` | string (ISO 8601) | always | When it was created. |
| `updatedAt` | string (ISO 8601) | always |  |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## PATCH /v1/apps/:name/schedules/:id

Changes a schedule. The kind of target (path or command) stays.

**Auth:** API key with full access (a key with scopes cannot call this route). An agent token of class `sandbox` or `twin` may call it; class `production` is refused (`agent-token-production-readonly`).

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The app's name. |
| `id` | The object's id. |

### Request body

JSON, the shape `SchedulePatch` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `schedule` | string | optional | A cron expression with five fields, or `@hourly`, `@daily` and the like. 1 to 200 characters. |
| `timezone` | string | optional | The IANA time zone the expression is read in. 1 to 64 characters. |
| `path` | string | optional | The app is woken and this path is called. 1 to 2048 characters. |
| `method` | `GET` \| `POST` | optional | With `path`: `GET` (default) or `POST`. |
| `command` | array of string | optional | Runs on a machine of its own, as an argument list. 1 to 64 items. |
| `timeoutSec` | integer | optional | The deadline in seconds: per attempt for a path, for the whole run for a command. 1 to 9007199254740991. |
| `memoryMb` | integer or null | optional | Memory in MB. |
| `vcpus` | integer or null | optional | Virtual CPUs. |
| `enabled` | boolean | optional | `false` creates the schedule paused. |

### Example request

```bash
curl -X PATCH "https://api.rytiv.app/v1/apps/<name>/schedules/<id>" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"schedule":"*/15 * * * *"}'
```

### Response

`200` with the schedule:

```json
{
  "id": "<id>",
  "app": "<app>",
  "schedule": "*/15 * * * *",
  "timezone": "Europe/Berlin",
  "timeoutSec": -9007199254740991,
  "enabled": true,
  "createdAt": "2026-10-21T09:00:00Z",
  "updatedAt": "2026-10-21T09:00:00Z"
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `id` | string | always | The schedule's id. |
| `app` | string | always |  |
| `schedule` | string | always | A cron expression with five fields, or `@hourly`, `@daily` and the like. |
| `timezone` | string | always | The IANA time zone the expression is read in. |
| `path` | string | if set | The app is woken and this path is called. |
| `method` | `GET` \| `POST` | if set | With `path`: `GET` (default) or `POST`. |
| `command` | array of string | if set | Runs on a machine of its own, as an argument list. |
| `timeoutSec` | integer | always | The deadline in seconds: per attempt for a path, for the whole run for a command. -9007199254740991 to 9007199254740991. |
| `memoryMb` | integer | if set | Memory in MB. -9007199254740991 to 9007199254740991. |
| `vcpus` | integer | if set | Virtual CPUs. -9007199254740991 to 9007199254740991. |
| `enabled` | boolean | always | `false` creates the schedule paused. |
| `nextRunAt` | string (ISO 8601) | if set | The next slot. |
| `lastRun` | object | if set | The last run, if any. |
| `lastRun.id` | string | always |  |
| `lastRun.schedule` | string | always | A cron expression with five fields, or `@hourly`, `@daily` and the like. |
| `lastRun.app` | string | always |  |
| `lastRun.scheduledFor` | string (ISO 8601) | always |  |
| `lastRun.trigger` | `schedule` \| `manual` | always |  |
| `lastRun.state` | `running` \| `succeeded` \| `failed` \| `skipped` \| `missed` | always |  |
| `lastRun.attempts` | array of object | always |  |
| `lastRun.statusCode` | integer | if set | -9007199254740991 to 9007199254740991. |
| `lastRun.error` | string | if set |  |
| `lastRun.job` | string | if set |  |
| `lastRun.exitCode` | integer | if set | -9007199254740991 to 9007199254740991. |
| `lastRun.missedSlots` | integer | always | 0 to 9007199254740991. |
| `lastRun.startedAt` | string (ISO 8601) | if set |  |
| `lastRun.finishedAt` | string (ISO 8601) | if set |  |
| `lastRun.durationMs` | integer | if set | 0 to 9007199254740991. |
| `createdAt` | string (ISO 8601) | always | When it was created. |
| `updatedAt` | string (ISO 8601) | always |  |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `bad-request` | The request is malformed or a field is out of range. `error` names every field; `issues` lists them. |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## DELETE /v1/apps/:name/schedules/:id

Removes the schedule and its run history.

**Auth:** API key with full access (a key with scopes cannot call this route). An agent token of class `sandbox` or `twin` may call it; class `production` is refused (`agent-token-production-readonly`).

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The app's name. |
| `id` | The object's id. |

### Example request

```bash
curl -X DELETE "https://api.rytiv.app/v1/apps/<name>/schedules/<id>" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`204` with no body.

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## GET /v1/apps/:name/schedules/:id/runs

The schedule's runs, newest first.

**Auth:** API key with full access or the scope `read`. Every class of agent token may call it.

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The app's name. |
| `id` | The object's id. |

### Query parameters

| Name | Description |
| --- | --- |
| `before` |  |
| `limit` | How many entries, at most. |

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/apps/<name>/schedules/<id>/runs" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with a list of runs, each:

```json
[
  {
    "id": "<id>",
    "schedule": "*/15 * * * *",
    "app": "<app>",
    "scheduledFor": "2026-10-21T09:00:00Z",
    "trigger": "schedule",
    "state": "running",
    "attempts": [
      {
        "attempt": 1,
        "startedAt": "2026-10-21T09:00:00Z",
        "durationMs": 0
      }
    ],
    "missedSlots": 0
  }
]
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `id` | string | always |  |
| `schedule` | string | always | A cron expression with five fields, or `@hourly`, `@daily` and the like. |
| `app` | string | always |  |
| `scheduledFor` | string (ISO 8601) | always |  |
| `trigger` | `schedule` \| `manual` | always |  |
| `state` | `running` \| `succeeded` \| `failed` \| `skipped` \| `missed` | always | `running`, `succeeded`, `failed`, `skipped` (a slot that came while the previous run was still going) or `missed`. |
| `attempts` | array of object | always |  |
| `statusCode` | integer | if set | -9007199254740991 to 9007199254740991. |
| `error` | string | if set |  |
| `job` | string | if set |  |
| `exitCode` | integer | if set | -9007199254740991 to 9007199254740991. |
| `missedSlots` | integer | always | 0 to 9007199254740991. |
| `startedAt` | string (ISO 8601) | if set |  |
| `finishedAt` | string (ISO 8601) | if set |  |
| `durationMs` | integer | if set | 0 to 9007199254740991. |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `bad-request` | The request is malformed or a field is out of range. `error` names every field; `issues` lists them. |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## POST /v1/apps/:name/schedules/:id/runs

Runs the schedule now, outside its expression.

**Auth:** API key with full access (a key with scopes cannot call this route). An agent token of class `sandbox` or `twin` may call it; class `production` is refused (`agent-token-production-readonly`).

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The app's name. |
| `id` | The object's id. |

### Example request

```bash
curl -X POST "https://api.rytiv.app/v1/apps/<name>/schedules/<id>/runs" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`202` (accepted, the work continues) with a JSON body.

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |
| 409 | `run-in-progress` |  |
| 410 | `gone` | The object existed and is gone. |

## POST /v1/apps/:name/jobs

Runs one command on a machine of its own with the app's image and environment. Answers `202` with the job.

**Auth:** API key with full access (a key with scopes cannot call this route). An agent token of class `sandbox` or `twin` may call it; class `production` is refused (`agent-token-production-readonly`).

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The app's name. |

### Request body

JSON, the shape `JobCreate` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `command` | array of string | required | The command as an argument list, run with the app's image and environment. 1 to 64 items. |
| `env` | object of string | optional | More environment for this job. |
| `timeoutSec` | integer | optional | Seconds before the job is ended. 1 to 3600, default `900`. |
| `links` | array of string | optional | Apps this job's machine may reach over the private network. 0 to 8 items. |
| `build` | string | optional | Run a specific build of the app instead of the one that is live. |
| `memoryMb` | integer | optional | Memory of the job's machine. Default: the app's. 128 to 16384. |
| `vcpus` | integer | optional | vCPUs of the job's machine. Default: the app's. 1 to 16. |
| `class` | any | optional | A named size instead of `vcpus`; the two must not contradict each other. |

### Example request

```bash
curl -X POST "https://api.rytiv.app/v1/apps/<name>/jobs" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"command":["<command>"]}'
```

### Response

`202` with the job:

```json
{
  "id": "<id>",
  "app": "my-app",
  "state": "queued",
  "exitCode": 0,
  "output": "<output>",
  "createdAt": "2026-10-21T09:00:00Z"
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `id` | string | always | The job's id. |
| `app` | string | always |  |
| `state` | `queued` \| `running` \| `done` \| `failed` \| `timeout` | always | `queued`, `running`, `done` (it ran; see `exitCode`), `failed` (it did not run to the end) or `timeout`. |
| `exitCode` | integer or null | always | The command's exit code once it ran; `null` otherwise. |
| `output` | string | always | What it wrote, cut at 1 MiB. |
| `build` | string | if set | The build it ran. |
| `machine` | string | if set | The machine it ran on. |
| `command` | array of string | if set | The command as given. |
| `startedAt` | string (ISO 8601) | if set | When the command started. |
| `finishedAt` | string (ISO 8601) | if set | When it ended. |
| `createdAt` | string (ISO 8601) | always | When it was created. |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `bad-request` | The request is malformed or a field is out of range. `error` names every field; `issues` lists them. |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## GET /v1/apps/:name/jobs

The app's jobs.

**Auth:** API key with full access or the scope `read`. Every class of agent token may call it.

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The app's name. |

### Query parameters

| Name | Description |
| --- | --- |
| `before` |  |
| `limit` | How many entries, at most. |

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/apps/<name>/jobs" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with a list of jobs, each:

```json
[
  {
    "id": "<id>",
    "app": "my-app",
    "state": "queued",
    "exitCode": 0,
    "output": "<output>",
    "createdAt": "2026-10-21T09:00:00Z"
  }
]
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `id` | string | always | The job's id. |
| `app` | string | always |  |
| `state` | `queued` \| `running` \| `done` \| `failed` \| `timeout` | always | `queued`, `running`, `done` (it ran; see `exitCode`), `failed` (it did not run to the end) or `timeout`. |
| `exitCode` | integer or null | always | The command's exit code once it ran; `null` otherwise. |
| `output` | string | always | What it wrote, cut at 1 MiB. |
| `build` | string | if set | The build it ran. |
| `machine` | string | if set | The machine it ran on. |
| `command` | array of string | if set | The command as given. |
| `startedAt` | string (ISO 8601) | if set | When the command started. |
| `finishedAt` | string (ISO 8601) | if set | When it ended. |
| `createdAt` | string (ISO 8601) | always | When it was created. |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## GET /v1/jobs/:id

One job with its state, exit code and output.

**Auth:** API key with full access or the scope `read`. Every class of agent token may call it.

### Path parameters

| Name | Description |
| --- | --- |
| `id` | The job's id. |

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/jobs/<id>" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with the job:

```json
{
  "id": "<id>",
  "app": "my-app",
  "state": "queued",
  "exitCode": 0,
  "output": "<output>",
  "createdAt": "2026-10-21T09:00:00Z"
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `id` | string | always | The job's id. |
| `app` | string | always |  |
| `state` | `queued` \| `running` \| `done` \| `failed` \| `timeout` | always | `queued`, `running`, `done` (it ran; see `exitCode`), `failed` (it did not run to the end) or `timeout`. |
| `exitCode` | integer or null | always | The command's exit code once it ran; `null` otherwise. |
| `output` | string | always | What it wrote, cut at 1 MiB. |
| `build` | string | if set | The build it ran. |
| `machine` | string | if set | The machine it ran on. |
| `command` | array of string | if set | The command as given. |
| `startedAt` | string (ISO 8601) | if set | When the command started. |
| `finishedAt` | string (ISO 8601) | if set | When it ended. |
| `createdAt` | string (ISO 8601) | always | When it was created. |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## DELETE /v1/jobs/:id

Cancels a job that still runs.

**Auth:** API key with full access (a key with scopes cannot call this route). An agent token of class `sandbox` or `twin` may call it; class `production` is refused (`agent-token-production-readonly`).

### Path parameters

| Name | Description |
| --- | --- |
| `id` | The job's id. |

### Example request

```bash
curl -X DELETE "https://api.rytiv.app/v1/jobs/<id>" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`204` with no body.

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |
