# Trainers

{{% notice style="note" title="Limited access" icon="lock" %}}
{{% include file="include/restricted_access.md" hidefirstheading="true"  %}}
{{% /notice %}}

## What a trainer is

A **trainer** is the instruction *run this training plan, on this data, with this configuration, now and maybe
again later*. Creating one is the only step a person takes to obtain a model: everything after the `201` — the
export, the training job, the model version, the inference image, the inferencer and its rule — happens on its
own and is described in [How it works](../how_it_works/).

| To | Call |
|---|---|
| Schedule a training | `POST /ai/organization/{organizationId}/trainer` |
| List the organization's trainers | `GET /ai/organization/{organizationId}/trainer` |
| Remove a trainer and its schedule | `DELETE /ai/organization/{organizationId}/trainer/{trainerId}` |

## Creating a trainer

```bash
curl --request POST \
     --header "X-ApiKey: <your-api-key>" \
     --header "Content-Type: application/json" \
     --data @trainer.json \
     https://api.opengate.es/ai/organization/acme/trainer
```

A trainer that learns the sessions of one APN from a time series and retrains every quarter:

```json
{
  "name": "radius-orange-trainer",
  "description": "Anomalous sessions on the corporate APN",
  "imageExecution": {
    "model": { "name": "radius-orange" },
    "trainingPlan": { "identifier": "6f1c2a4e-…" },
    "configuration": { "apn": "corporate.example.apn" },
    "timeout": 1800
  },
  "source": {
    "timeserie": {
      "id": "683fee1f2b447a7318586445",
      "filter": { "gte": { "at": "2026-06-01T00:00:00Z" } },
      "columns": [
        { "column": "apn",           "output": { "name": "APN" } },
        { "column": "state",         "output": { "name": "SessionState" } },
        { "column": "ggsnIp",        "output": { "name": "IP_GGSN" } },
        { "column": "deviceIp",      "output": { "name": "IP_Device" } },
        { "column": "sentBytes",     "output": { "name": "sbytes", "parquet": { "type": "LONG" } } },
        { "column": "receivedBytes", "output": { "name": "dbytes", "parquet": { "type": "LONG" } } },
        { "column": "sentPackets",   "output": { "name": "spkts",  "parquet": { "type": "LONG" } } },
        { "column": "receivedPackets", "output": { "name": "dpkts", "parquet": { "type": "LONG" } } },
        { "column": "duration",      "output": { "name": "dur",    "parquet": { "type": "LONG" } } }
      ],
      "timeout": 6000
    }
  },
  "schedule": {
    "expression": "0 0 0 15 */3 ? *",
    "isImmediateExecution": true
  },
  "createRule": true
}
```

The response is `201 Created` with a `Location` header holding the trainer's identifier.

### Field by field

| Field | Required | Meaning |
|---|---|---|
| `name` | yes | The trainer's name, unique in the organization. Names the Kubernetes secret that carries your credentials to the job |
| `description` | no | Free text |
| `imageExecution.model.name` | yes | The **model name**: `^[a-z0-9][a-z0-9-]*$`. Only one trainer per model name per organization. It becomes the name of the inferencer, the rule and the scheduler entry — see the [naming table](../how_it_works/#naming-in-one-table) |
| `imageExecution.trainingPlan.identifier` | yes | The plan to run, from the [catalogue](../training_plans/). Must exist |
| `imageExecution.configuration` | per plan | Key–value pairs, one per entry in the plan's `configFields`. Handed to the job as environment variables |
| `imageExecution.timeout` | yes | Seconds the training job may run before it is killed. The autoencoder and the image plan need far more than the forest |
| `source` | yes | Exactly one of `path` or `timeserie`, below |
| `schedule` | no | When and how often to run. Absent: once, about a minute from now |
| `createRule` | no | Whether the first training should create the plan's rule. Default `true` |

Read-only fields come back on `GET`: `identifier`, `orgName`, `hasRetraining` and `schedule.schedulerId` — the
identifier of the entry the scheduler created, which is also the model name.

## Data sources

### A file in your file space

```json
"source": { "path": "radius/sessions-2026-q2.parquet" }
```

`path` is relative to the root of your organization's [file space](../file_connector/), where you upload it
beforehand. Inside the job the file space is mounted at `/data`, so the plan reads `/data/radius/sessions-2026-q2.parquet`.
A path may also be a **folder**, which is what the [image plan](../training_plans/image_anomaly_detection/) expects.

A file source schedules a single **image execution** in the scheduler.

### A time series

```json
"source": {
  "timeserie": {
    "id": "<time series identifier>",
    "filter": { "…": "…" },
    "columns": [ { "column": "<yours>", "output": { "name": "<the plan's>", "parquet": { "type": "LONG" } } } ],
    "timeout": 6000
  }
}
```

| Field | Meaning |
|---|---|
| `id` | The time series to export, from the organization's [time series](../../data_retrieval/time_series/) |
| `filter` | Optional. A time series filter restricting the rows, for example to a date range |
| `columns` | Which of your columns feed the plan, and under what name. `output.name` must be one of the names the plan lists in `columnData`; `output.parquet.type` fixes the Parquet type when the default is not right |
| `timeout` | Seconds to wait for the export. The scheduler waits five seconds more than this for the export's callback |

A time-series source schedules a **pipeline**: first the platform's own
[Parquet export](../../data_retrieval/time_series/querying/#parquet-export) of that time series, writing
`<model>-<plan>-<time series>.parquet` into your file space, then the training image with `dataSourcePath`
pointing at it. Each retraining exports again, so the model always learns from current data.

## Schedules

```json
"schedule": { "expression": "0 0 0 15 */3 ? *", "isImmediateExecution": true }
```

- `expression` — a cron expression. Standard five fields work; the scheduler also accepts a leading **seconds**
  field and a trailing **year** field, and `?` in the day fields. `0 0 0 15 */3 ? *` is *00:00:00 on the 15th
  of every third month*. Time zone is UTC.
- `isImmediateExecution` — also run **now**, without waiting for the first tick. The console sets it, so a new
  trainer always produces a first version straight away.

**No `schedule` at all** means a single execution about one minute after creation, and `hasRetraining: false`.
An expression that pins one instant — every field numeric, including the year — is treated the same way.

## What the platform does with your request

Knowing this helps when something does not appear where you expect it.

1. Rejects the request if another trainer in the organization has the same `name` or the same model name, or if
   the plan is not in the catalogue.
2. Creates a Kubernetes secret named `<organization>-<trainer name>-env-secret` holding **your API key** and the
   organization name. The training job uses it to register the inferencer, create the rule and report back —
   everything the job does, it does as you.
3. Asks the scheduler for an image execution (file source) or a pipeline (time-series source) named after the
   model, running the plan's image with these environment variables: your `configuration` entries, `modelName`,
   `createRule`, `minDataToTrain` from the plan, and `dataSourcePath`; plus the secret above and the platform's
   shared AI settings. The scheduler waits for the job's callback for `timeout` + 5 seconds.
4. Stores the trainer and answers `201`.

The trainer record is a **schedule**, not a status: to know whether a training ran and how it went, read the
scheduler's [history](../scheduler/#execution-history) for `schedulerId` = model name, or open *See history* in
the console.

## Listing trainers

```bash
curl --request GET \
     --header "X-ApiKey: <your-api-key>" \
     https://api.opengate.es/ai/organization/acme/trainer
```

Returns every trainer of the organization, with the read-only fields filled in. An organization with no trainers
gets an empty list, not an error.

## Removing a trainer

```bash
curl --request DELETE \
     --header "X-ApiKey: <your-api-key>" \
     https://api.opengate.es/ai/organization/acme/trainer/<trainerId>
```

Removes the schedule from the scheduler, the credentials secret and the trainer record, and answers `204`. A
training that is running is not interrupted — cancel it through the [scheduler](../scheduler/#cancelling-a-running-execution)
if you need to. **The inferencer, its model versions and its rule are not touched**: they are separate
resources, removed through the [Inferencers API](../inferencers/). Deleting the trainer only means no further
version will be trained.

## Errors

Errors follow the platform's usual shape, a list of `code`, `message` and `context`:

| Situation | Status |
|---|---|
| A trainer with that `name`, or that model name, already exists in the organization | `400` |
| The training plan identifier is not in the catalogue | `400` |
| The body fails the specification — a model name with uppercase letters, both `path` and `timeserie`, a missing `timeout` | `400` |
| The organization does not exist, or the trainer to delete does not | `404` |

## API specification

{{< openapi src="trainers.yaml" >}}
