Run an SOP from a bench station
Start a run of the SOP in force, confirm steps with scans, record measurements with their units, and close the run.
Scopes: read:sops, write:sop_runs, write:sop_steps, and read:sop_runs to read a run back. Endpoints: Get an SOP, Start an SOP run, Record step evidence, Record a step measurement, Complete an SOP run, Get an SOP run, List a run's evidence.
A bench station (a tablet, a Pi with a touchscreen, a scanner) can walk a technician through an SOP and record what actually happened: who confirmed each step, what was scanned, which pH was measured.
1. Load the SOP
curl "https://app.xplantpro.com/api/v1/sops/$SOP_ID" \
-H "Authorization: Bearer $XPLANT_API_KEY"You always get the version the lab works from right now. You never get a draft, or a version that has been approved but hasn't taken effect yet. If no version is in force, version is null and there are no steps: show that to the technician rather than presenting an empty procedure.
currentVersion on the SOP is the highest version number that exists; version.version is the one in force. They often differ.
2. Start a run
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": "9d2c4b6a-8e1f-4a3b-b5c7-d9e1f3a5b7c9", "batch_code": "WK-38"}'If your lab enforces SOP training, starting a run checks the key owner's training for this SOP. With enforcement set to block, an untrained owner gets 403 TRAINING_REQUIRED and no run starts. Set to warn, the run starts (201) and the response carries meta.training_warning with the qualification and its expires_on date; show it to the technician.
There is deliberately no version argument. A run always follows the version in force, because a run is a record of what someone did, and what they did was follow the lab's current procedure. An SOP with no version in force answers 409 SOP_RUN_NOT_EFFECTIVE (not 404: the SOP exists, it just can't be run yet).
3. Record evidence against steps
A scanner confirms a step:
curl -X POST "https://app.xplantpro.com/api/v1/sop-runs/$RUN_ID/steps/step-3/events" \
-H "Authorization: Bearer $XPLANT_API_KEY" \
-H "Idempotency-Key: station-3-wk38-step3-scan" \
-H "Content-Type: application/json" \
-d '{"event_type": "scanned", "payload": {"code": "LINE-0412"}}'Step events cover confirmations, scans, skips, notes and device states.
A meter posts a reading against the same step. The unit is required and has no default: a number without a unit isn't a measurement.
curl -X POST "https://app.xplantpro.com/api/v1/sop-runs/$RUN_ID/steps/step-3/measurements" \
-H "Authorization: Bearer $XPLANT_API_KEY" \
-H "Idempotency-Key: station-3-wk38-step3-ph" \
-H "Content-Type: application/json" \
-d '{"metric": "ph", "value": 5.7, "unit": "pH"}'Measurements appear on the run's timeline as measured events, alongside the confirmations, so Get an SOP run reads as one story from start to finish.
4. Close the run
When the work is done, end the run with an outcome: completed when the procedure was carried out, failed when it was carried out and didn't work, or cancelled when it stopped before the end. notes is optional and is added after any notes already on the run.
curl -X POST "https://app.xplantpro.com/api/v1/sop-runs/$RUN_ID/complete" \
-H "Authorization: Bearer $XPLANT_API_KEY" \
-H "Idempotency-Key: station-3-wk38-complete" \
-H "Content-Type: application/json" \
-d '{"outcome": "completed", "notes": "All vessels labelled and moved to shelf 3."}'The run then looks exactly like one closed in xPlant. Its status is the outcome, and completedAt is set for completed (empty for failed and cancelled). A failed run raises the same deviation alert it would in xPlant.
Only the person who started the run, or a manager or above, can close it, and the key's owner must be one of them. For anyone else the answer is 404 NOT_FOUND, the same as for a run that doesn't exist. A run that has already ended answers 409 SOP_RUN_CLOSED.
Reading a run back
In the SDK, sopRuns.get() returns the whole trail in run.events, however long it is. For a very long run, sopRuns.listEvents() hands it to you a page at a time instead:
const run = await client.sopRuns.get(runId);
console.log(`${run.events.length} pieces of evidence`);
// Or a page at a time:
for await (const event of client.sopRuns.listEvents(runId)) {
console.log(event.recordedAt, event.stepKey, event.eventType);
}Over plain HTTP, Get an SOP run returns the run, its steps and the first 50 pieces of evidence. If there are more, meta.events_next_cursor isn't null. Pass it as cursor to List a run's evidence, and keep following meta.next_cursor until it's null:
first = requests.get(f"{BASE}/sop-runs/{run_id}", headers=HEADERS, timeout=10).json()
events = first["data"]["events"]
cursor = first["meta"]["events_next_cursor"]
while cursor:
page = requests.get(
f"{BASE}/sop-runs/{run_id}/events",
headers=HEADERS,
params={"cursor": cursor},
timeout=10,
).json()
events += page["data"]
cursor = page["meta"]["next_cursor"]To read the whole trail from the start, call List a run's evidence without a cursor. A cursor only works on the run it came from.
Runs are append-only
Nothing on a run can be edited or deleted. To correct a mistake, record another event that says so. Once a run has ended (completed, failed, cancelled or archived), further writes answer 409 SOP_RUN_CLOSED: its record is what happened. A step that isn't part of the run's version answers 404 NOT_FOUND.
Use an Idempotency-Key on every write from a station. Bench networks are flaky, and a retried scan must not count twice.
Bench stations and keys
Device tokens only cover sensor readings, device events and heartbeats, so a station that runs SOPs needs a workspace key. Keep the damage from a lost or copied station small:
- Give each station its own key, named after the station, holding only what it uses. For SOP runs that's
read:sops,write:sop_runsandwrite:sop_steps, plusread:sop_runsif it reads runs back; addread:labelsandwrite:label_scansif it scans, andwrite:transfersif it records transfers. - Keep the key in the station's configuration or secrets store, never in code or a shared repository.
- If a station goes missing, revoke its key in Settings → Integrations → API Keys. Nothing else stops working.