# Start an SOP run

> Starts a run of an SOP: the record that a procedure was carried out, step by step, against the version the lab had in force.

Source: https://docs.xplantpro.com/docs/api/sops/create-sop-run

`POST https://app.xplantpro.com/api/v1/sop-runs`

- Required scope: `write:sop_runs`
- Credentials: workspace API key (`xpk_`)
- Idempotency-Key: honoured (24 hours)

A bench station typically:

1. Reads the SOP with Get an SOP and shows the steps of the version in force.
2. Starts a run here, naming the batch or line it is for in `batch_code`.
3. Posts evidence against each step as the work happens — a confirmation, a scanned vessel label, a reading from a balance or pH meter — with Record a step event and Record a step measurement.

The run follows the version in force when it starts, and you cannot choose another. Send an `Idempotency-Key` so a retried start does not open a second run.

The run is recorded as the key's owner, and your lab's training rule applies to them exactly as it does in xPlant. Where the lab requires current training before an SOP is run, a key whose owner has none on this SOP is refused. Where the lab only asks for a warning, the run starts and `meta.training_warning` says what is missing, so the station can tell the person running it.

See also: [Run an SOP from a bench station](https://docs.xplantpro.com/docs/guides/sop-runs.md).

Send an `Idempotency-Key` header to make retries safe: a repeat with the same key within 24 hours returns the first response instead of writing twice. See [Idempotency](https://docs.xplantpro.com/docs/idempotency.md).

## Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | No | Any unique string you choose per logical write. A retry carrying the same key within 24 hours returns the first result instead of writing again. Scoped to your API key and this operation. 8–255 characters. Must match `^[A-Za-z0-9._:~-]+$`. |

## Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `sop_id` | string (uuid) | Yes | The SOP to run. It must have a version in force. |
| `batch_code` | string | No | Your lab's own identifier for what is being run, such as a batch or line code. Up to 120 characters. |
| `plant_id` | string (uuid) | No | The plant the run is for, when there is one. It must be in this workspace. |

## Example

**curl**

```bash
curl -X POST https://app.xplantpro.com/api/v1/sop-runs \
  -H "Authorization: Bearer $XPLANT_API_KEY" \
  -H "Idempotency-Key: station-3-wk38-start" \
  -H "Content-Type: application/json" \
  -d '{
    "sop_id": "2e4f6a8c-0b1d-4e3f-9a5b-7c9d1e3f5a7b",
    "batch_code": "LINE-0412"
  }'
```

**JavaScript**

```js
import { XPlantClient } from "@shmaplex/xplant-sdk";

const client = new XPlantClient({ apiKey: process.env.XPLANT_API_KEY });

const run = await client.sopRuns.start(
  {
    sop_id: "2e4f6a8c-0b1d-4e3f-9a5b-7c9d1e3f5a7b",
    batch_code: "LINE-0412",
  },
  { idempotencyKey: "station-3-wk38-start" },
);
if (run.trainingWarning) {
  // e.g. { qualification: "expiring", expires_on: "2026-10-01" }: show it to the technician
}
```

**Python**

```python
import os
import requests

resp = requests.post(
    "https://app.xplantpro.com/api/v1/sop-runs",
    headers={
        "Authorization": f"Bearer {os.environ['XPLANT_API_KEY']}",
        "Idempotency-Key": "station-3-wk38-start",
    },
    json={
        "sop_id": "2e4f6a8c-0b1d-4e3f-9a5b-7c9d1e3f5a7b",
        "batch_code": "LINE-0412",
    },
    timeout=10,
)
body = resp.json()
if not body["ok"]:
    raise RuntimeError(f"{resp.status_code} {body['code']}: {body['error']}")
run = body["data"]
```

## Response

`201` with `{ "ok": true, "data": … }`. `data` holds the result.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | Yes | — |
| `sopId` | string \| null (uuid) | Yes | The SOP being run. |
| `version` | integer \| null | Yes | The version of the SOP this run follows — the one in force when it started. It never changes afterwards. |
| `versionId` | string \| null (uuid) | Yes | That version's id. |
| `status` | string | Yes | `in_progress` from the moment a run starts. A run is completed in xPlant, once every step is ticked off; runs can also be `pending`, `on_hold`, `failed`, `cancelled` or `archived`. |
| `batchCode` | string \| null | Yes | Your lab's own identifier for what is being run. |
| `startedAt` | string \| null | Yes | — |
| `completedAt` | string \| null | Yes | — |
| `progress` | number | Yes | How far through its steps the run is, from 0 to 100, as ticked off in xPlant. |
| `steps` | any[] | Yes | The step checklist as ticked off in xPlant, one entry per step, such as `{ "id": "step-3", "completed": true }`. Empty until someone works the run in xPlant. Evidence posted through the API is in `events` and does not tick steps off here. |

```json title="Response"
{
  "ok": true,
  "data": {
    "id": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
    "sopId": "2e4f6a8c-0b1d-4e3f-9a5b-7c9d1e3f5a7b",
    "version": 2,
    "versionId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
    "status": "in_progress",
    "batchCode": "LINE-0412",
    "startedAt": "2026-09-25T08:02:13.000Z",
    "completedAt": null,
    "progress": 0,
    "steps": []
  }
}
```

The envelope can also carry `meta`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `training_warning` | object | Yes | Sent when your lab enforces training and the key's owner's training on this SOP needs their attention: it is missing, expired or revoked and the lab warns rather than refuses, or it is current but lapses within 30 days. The run has started either way. Left out otherwise. |
| `training_warning.qualification` | `"untrained"` \| `"expired"` \| `"revoked"` \| `"expiring"` | Yes | Where the key's owner stands on this SOP: `untrained` when no training is recorded, `expired` when it has lapsed, `revoked` when it was withdrawn, `expiring` when it is current but lapses within 30 days. |
| `training_warning.expires_on` | string \| null | Yes | The day that training lapsed or lapses, as `YYYY-MM-DD`, for `expired` and `expiring`. Null otherwise. |

| Response header | Meaning |
| --- | --- |
| `Idempotent-Replay` | `true` when this response is a replay of an earlier request with the same `Idempotency-Key`. |
| `X-Request-Id` | Identifies this request. Include it when you contact support. |

## Errors

| Status | Code | When |
| --- | --- | --- |
| `400` | `VALIDATION_ERROR` | The request body is not valid JSON. |
| `401` | `UNAUTHORIZED` | The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. |
| `402` | `PAID_PLAN_REQUIRED` | The workspace's plan does not include the API. The full API is included with xPlant+ Teams and Enterprise; on Hobby and Pro Lab, keys can connect devices only. |
| `403` | `FORBIDDEN` | The key lacks a scope this operation requires, or its owner's current role in the workspace cannot use it — a key never does more than its owner can in xPlant. `error` names the scope, and for a role, the role it needs. |
| `403` | `DEVICE_TOKEN_NOT_ACCEPTED` | A device token was sent; this operation needs a workspace API key. |
| `403` | `TRAINING_REQUIRED` | Your lab requires current training before an SOP is run, and the key's owner has none on this one: it was never recorded, has expired, or was revoked. Nothing is started. Once their training is recorded in xPlant, start the run again. |
| `404` | `NOT_FOUND` | The SOP, or the plant named in `plant_id`, is not in this workspace. |
| `409` | `IDEMPOTENCY_IN_FLIGHT` | A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds. |
| `409` | `SOP_RUN_NOT_EFFECTIVE` | The SOP has no version in force, so there is nothing approved to run. Put a version into force in xPlant, then start the run. |
| `422` | `VALIDATION_ERROR` | The `Idempotency-Key` header is malformed. |
| `422` | `VALIDATION_ERROR` | A field failed validation, for example `sop_id` is missing or is not an id. `error` gives the first problem but does not name the field. |
| `429` | `RATE_LIMIT_EXCEEDED` | Too many requests for this key, device token or workspace. Wait `Retry-After` seconds. |
| `500` | `SOP_RUN_CREATE_FAILED` | The run could not be started. Retry with the same `Idempotency-Key`. |

Branch on `code`, never on the `error` text. See [Errors](https://docs.xplantpro.com/docs/errors.md).
