# Jobs

A **job** is one execution of an operation type over a target set of entities. Creating a job is the
normal way to run an operation: you `POST` a job request, and OpenGate turns it into one operation per
target entity.

```bash
POST /v80/operation/jobs
```

## Anatomy of a job request

| Section | Purpose |
|---|---|
| `name` | The operation type to execute, for example `REBOOT_EQUIPMENT`. |
| `parameters` | The parameters of the operation itself. See [Operation parameters](../parameters/). |
| `target` | Which entities the operation runs on. |
| `active` | Whether the job starts. `false` creates the job without launching it. |
| `schedule` | When the job runs and when it gives up. |
| `operationParameters` | Timeouts and retry policy applied to each individual operation. |
| `notify` | Whether to notify the operation result by email or trap. Defaults to `false`. |
| `callback` | URI to be notified on job progress instead of polling. See [Callbacks](../callbacks/). |
| `userNotes` | Free-text notes attached to the job instance. |

## Selecting the target

There are three ways to reference the entities a job acts on, and they are **mutually exclusive** —
a single job cannot mix entity lists, tags and filters.

### An explicit list of entities

```json
"target": { "append": { "entities": ["device_1", "device_2"] } }
```

The same parametrization is applied to every entity in the list. Two limits apply:

- **Entities per array**: 100 by default.
- **Request body size**: 300 KBytes by default.

Both are configurable per platform, so check the values with your administrator. To launch an
operation over a larger list, create the job with `active` set to `false` and use `PUT` requests to
append entities in batches, activating the job in the last call.

### A tag

```json
"target": { "append": { "tags": ["fleet_north"] } }
```

Only one tag name can be passed. OpenGate resolves the tag into the target set and builds the
internal structure of the job; when that work finishes the job waits in `IDLE` if it is not active,
or in `SCHEDULED` if it is.

### A filter

```json
"target": { "append": { "filter": { "eq": { "device.model": "MDL-1" } } } }
```

The filter is evaluated by the platform to resolve the target set. Previously created filters cannot
be reused here — the filter must be inlined in the request.

The optional `resourceType` query string parameter restricts the filter to one entity type:

| Value | Entities considered |
|---|---|
| `entity.device` | Devices |
| `entity.asset` | Assets |
| `entity.commsModule` | Communication modules |
| `entity.subscription` | Subscriptions |
| `entity.subscriber` | Subscribers |

{{% notice style="warning" title="Filter targets cannot be updated" icon="triangle-exclamation" %}}
The `target` section of a filter-based job cannot be modified afterwards. To change the target set,
deactivate the job and create a new one with the new filter.
{{% /notice %}}

## Scheduling

The `schedule` block controls when the job runs. `start` accepts either an exact `date` or a
`delayed` value in milliseconds; `stop` sets the deadline after which pending operations are
cancelled.

Two optional modes refine *how* the operations are distributed inside that window.

### Window

`window` restricts execution to given weekdays and a daily time range — useful when field
interventions are only acceptable during a maintenance window.

Periods must be **whole hours**; intermediate periods are rejected:

| Window | Allowed |
|---|---|
| `"start": "09:00:00Z"` — `"stop": "15:00:00Z"` | Yes |
| `"start": "09:30:00Z"` — `"stop": "15:30:00Z"` | Yes |
| `"start": "09:00:00Z"` — `"stop": "15:30:00Z"` | No |

### Scattering

`scattering` spreads the individual operations across the available time instead of firing them all
at once. It exists to protect shared infrastructure — typically a mobile operator cell that would
collapse if thousands of devices woke up simultaneously.

| Field | Meaning |
|---|---|
| `maxSpread` | Percentage (0–100) of the job's effective time used to spread operations. `0` runs as fast as possible, `100` spreads over the whole window. Default `0`. |
| `strategy.field` | Entity field used to group operations. Currently only `subscription.collected.cellInfo`. |
| `strategy.factor` | Dispersion level (0–100) applied within each group. `0` clusters maximally, `100` scatters maximally. Default `0`. |
| `strategy.warningMaxRate` | Speed control in operations per second, to verify the resulting rate stays within `maxSpread`. |

### Per-operation timeouts and retries

`operationParameters` applies to each individual operation, not to the job as a whole:

| Field | Meaning |
|---|---|
| `ackTimeout` | Milliseconds to wait for the device to accept the operation. On expiry the operation is cancelled. |
| `timeout` | Milliseconds to wait for the operation to finish. Default `60000`. |
| `retries` | Number of retries when the operation gets no acknowledgement or times out. Default `0`. |
| `retriesDelay` | Milliseconds between retries. |
| `retryResultList` | Results that trigger a retry. `ERROR_TIMEOUT` is always included. |

{{% notice style="info" title="Minimum internal timeout" icon="lightbulb" %}}
OpenGate enforces a minimum internal timeout of 40 seconds, so `timeout` plus `ackTimeout` must be
greater than that. The default of 60 seconds is a good starting point.
{{% /notice %}}

## Job life cycle

A job's `status` reflects the aggregate progress of all its operations:

```mermaid
stateDiagram-v2
    direction TB
    [*] --> IDLE: active=false
    [*] --> SCHEDULED: active=true<br>start delayed
    [*] --> IN_PROGRESS: active=true<br>start now

    IDLE --> SCHEDULED: active=true<br>start delayed
    IDLE --> IN_PROGRESS: active=true<br>start now
    SCHEDULED --> IDLE: active=false
    SCHEDULED --> IN_PROGRESS: start time<br>reached

    IN_PROGRESS --> PAUSED: active=false
    PAUSED --> IN_PROGRESS: active=true

    IN_PROGRESS --> FINISHED: all ok
    IN_PROGRESS --> FINISHED_WITH_ERRORS: with errors
    IN_PROGRESS --> CANCELLING_BY_USER: cancelled<br>by a user
    IN_PROGRESS --> CANCELLING_BY_ENGINE: timeout<br>reached
    SCHEDULED --> CANCELLING_BY_USER: cancelled<br>by a user
    CANCELLING_BY_USER --> CANCELLED: all operations<br>cancelled
    CANCELLING_BY_ENGINE --> TIMEOUT_CANCELLED: all operations<br>cancelled

    FINISHED --> [*]
    FINISHED_WITH_ERRORS --> [*]
    TIMEOUT_CANCELLED --> [*]
    CANCELLED --> [*]
```

| Transition | Trigger |
|---|---|
| Into `IDLE` | The job is created or updated with `active` set to `false`. |
| Into `SCHEDULED` | The job is active and its `schedule.start` is a date or a delay. |
| Into `IN_PROGRESS` | The job is active with an immediate start, or the scheduled start time is reached. |
| `IN_PROGRESS` → `PAUSED` | `active` set to `false` on a running job. |
| `PAUSED` → `IN_PROGRESS` | `active` set to `true` on a paused job. |
| Into `FINISHED` | Every operation reached a final state successfully. |
| Into `FINISHED_WITH_ERRORS` | Operations failed or were cancelled. |
| Into `CANCELLING_BY_USER` | A user cancels the job, through the console or the API. |
| Into `CANCELLING_BY_ENGINE` | The job's timeout is reached, so the platform cancels it. |
| Into `CANCELLED` | Every entity operation of a user-cancelled job finished cancelling. |
| Into `TIMEOUT_CANCELLED` | The same, for a job the timeout cancelled. |

Both cancelling states are transient: the job stays there until every one of its operations has finished
cancelling, which on a job targeting thousands of entities is not instant.

{{% notice style="note" title="One detail still unconfirmed" icon="circle-info" %}}
The specification defines what each state means but not which terminal state the engine path ends in. The
pairing above — a user cancellation ending in `CANCELLED`, a timeout ending in `TIMEOUT_CANCELLED` — follows
from their descriptions and is pending confirmation.
{{% /notice %}}

See the [status reference](../status_reference/) for the complete list of job, operation and step
values.

## Reading the result

An execution involves as many entities as the target references, so **one job explodes into many
results**. The API exposes both levels:

```mermaid
flowchart LR
    JOB["Job"] --> SUM["report.summary<br>one aggregated view<br>counters per state"]
    JOB --> RES["operations<br>one result per entity<br>status, result, steps"]
```

| Endpoint | Returns |
|---|---|
| `GET /v80/operation/jobs/{jobId}` | The job request plus `report.summary` |
| `GET /v80/operation/jobs/{jobId}/operations` | Paginated per-entity results |
| `GET /v80/operation/jobs/{jobId}/operations/{id}` | A single entity's result |

The per-entity list is paginated with `start` and `size` parameters — necessary when a job targets
thousands of entities. Each operation object carries its own `status`, `result`, `description` and
`steps` array.

To be notified when the job starts and when it finishes instead of polling these endpoints, configure
a [callback](../callbacks/).

## Updating a job

```bash
PUT /v80/operation/jobs/{jobId}
```

A job can only be modified while `active` is `false` **and** it has not started. What you can change:

- Request fields: `active`, `notify`, `callback`, `userNotes`, `schedule.start`, `schedule.stop`.
- The target entity list, by appending or removing entities.

The same JSON size limit as in creation applies. In the last `PUT`, set `active` to `true` to start
the execution.

### Pause and resume

The same endpoint controls a running job through the `active` field:

- **Pause**: set `active` to `false` on a job in `IN_PROGRESS`. The job moves to `PAUSED`. While
  paused, the job's features cannot be modified.
- **Resume**: set `active` to `true` on a paused job. The job returns to `IN_PROGRESS`.

## Cancelling a job

```bash
DELETE /v80/operation/jobs/{jobId}
```

The job moves to `CANCELLING_BY_USER` first — or to `CANCELLING_BY_ENGINE` when the platform itself
cancels it — and to `CANCELLED` once all of its operations are cancelled.

{{% notice style="warning" title="Cancellation does not roll back" icon="triangle-exclamation" %}}
Cancelling a job does not undo steps that already executed on the devices. A firmware update
cancelled halfway leaves the device halfway. Be deliberate.
{{% /notice %}}

## Searching jobs and operations

Job and operation searches follow the platform's standard search pattern, with support for filtering,
sorting, field selection and summaries:

```bash
POST /v80/search/jobs
POST /v80/search/jobs/summary
POST /v80/search/entities/devices/operations
POST /v80/search/entities/operations/history
```

Equivalent endpoints exist for subscribers and subscriptions. Results are returned as JSON by
default, or as CSV through HTTP header options. The full parameter list is in the
[API reference](../api_reference/).
