From the Business tier, the GraphQL endpoint supports 12 write mutations: create, update and delete on Work Orders, Assets and Parts, plus adjustStock for inventory movements. Mutations go through the same services the application itself uses, so audit logs, notifications and reference auto-generation (WO-XX, ASS-XX, PART-XX) all behave identically. The eleventh, addMeterReading, records meter readings (running hours, counters) — see below. The twelfth, setPurchaseOrderErpReference, writes your ERP's own document number onto a purchase order (FULL-access key only) and fires no webhook, so your ERP never receives its own write back.
Available mutations
- createWorkOrder(input) → WorkOrder
- updateWorkOrder(id, input) → WorkOrder
- deleteWorkOrder(id) → DeleteResult
- createAsset(input) → Asset
- updateAsset(id, input) → Asset
- deleteAsset(id, force?) → DeleteResult
- createPart(input) → Part
- updatePart(id, input) → Part
- deletePart(id, force?) → DeleteResult
- adjustStock(id, quantity, reason?, vendorId?, documentNumber?) → Part
- addMeterReading(meterId, value?, incrementValue?, date?, notes?, confirmRegression?) → MeterReading
- setPurchaseOrderErpReference(id, erpReference?) → PurchaseOrder
Input shape
Each mutation has an InputType with safe fields only — companyId, sequenceNumber, reference and createdById are filled server-side. You only pass title, description, status, priority, assetId, locationId, etc. Part inputs also accept barcode, maxStockLevel, locationId and vendorId, and quantity/minQuantity/maxStockLevel take decimals (e.g. 0.5). Enumerated fields are checked: priority must be one of LOW, MEDIUM, HIGH, CRITICAL; a work order's status one of OPEN, SCHEDULED, IN_PROGRESS, ON_HOLD, COMPLETED, VALIDATED, CANCELLED, CLOSED; an asset's status one of OPERATIONAL, DEGRADED, DOWN, MAINTENANCE, STANDBY, RETIRED. Anything else is refused with BAD_REQUEST. Part inputs also accept customReference — your own code for the part (its ERP code, for instance), which part(barcode:) then matches. It is not enforced unique: that is yours to guarantee.
Example: create a work order
Create a work order: mutation($i: WorkOrderCreateInput!) { createWorkOrder(input: $i) { id title reference status createdAt } } with the variables {"i": {"title": "Pump #3 leak", "priority": "HIGH"}}
Adjusting stock
To change a part's quantity, use adjustStock with a signed delta — negative to consume, positive to receive. It updates the quantity and writes a StockMovement audit row in one atomic transaction, and never lets stock fall below zero. For a part whose stock is tracked per location, pass locationId: the movement is applied to that location, and without it the call is refused: errors[0].extensions.code is BAD_REQUEST and extensions.reason is LOCATION_STOCK_REQUIRES_LOCATION. updatePart(quantity) sets an absolute value instead: it is refused on a part tracked per location for the same reason, and otherwise records the difference as an ADJUSTMENT movement. You can also record where the goods came from: vendorId and documentNumber (the supplier's invoice or delivery-note number, 64 characters max) are stored on the movement itself, so the history shows who supplied them and under which document. Both are optional — an ordinary consumption sends neither. The supplier must belong to your company.
Recording meter readings
addMeterReading records a reading on a meter — running hours, a counter, an odometer, a gauge — so an external system (SCADA, PLC gateway, ERP) can feed meter-based preventive maintenance without anyone typing the number in. First find the meter id: query { meters(assetId: 7) { id reference name unit type lastReadingValue lastReadingDate } } Then send EITHER value (the absolute total the meter shows) OR incrementValue (for example the hours run since the last reading, which the server adds to the previous reading) — exactly one of the two: mutation { addMeterReading(meterId: 12, value: 1250.5, date: "2026-09-25T06:00:00Z") { id value date isAnomaly } } date is optional (ISO 8601, defaults to now) and may be in the past, never in the future. On RUNTIME and COUNTER meters, a value lower than the previous reading is refused with BAD_REQUEST — after a meter reset or replacement, send it again with confirmRegression: true. The reading goes through the same service as the meter screen in the app: if it is the latest one, meter triggers and meter-based preventive maintenance fire exactly as they do for a reading typed in by a technician. This mutation needs a FULL-access key.
Attribution
Every mutation runs as the key's owner — the person the key acts as — with their permissions, their data visibility and their name in the audit log. A key never approves a workflow step, even when its owner could.
Retrying safely (Idempotency-Key)
A network timeout does not tell you whether a mutation ran. Send an Idempotency-Key header (8 to 64 letters, digits or dashes — a UUID works) with every request that writes, and send the SAME key when you retry it. For 7 days, a retry with the same key returns the stored result of the first call (the response carries "Idempotent-Replayed: true") instead of running the mutation again: no second work order, no doubled stock movement.
- One key per write. In a request with several aliased mutations, each mutation is recorded on its own under that key. The values sent are not compared: the same key with different values returns the result of the first call.
- A call that failed keeps nothing: retrying it with the same key runs it again.
- IDEMPOTENCY_KEY_REUSED — under this key, the same name (field or alias) already ran a different mutation, or another caller used the key: use a new key for a new write.
- IDEMPOTENCY_IN_PROGRESS — the first call is still running: wait a few seconds and retry.
Common errors
- GRAPHQL_VALIDATION_FAILED — the input shape does not match (for example a required title is missing)
- BAD_REQUEST — value validation failed (for example a title over 500 characters)
- FORBIDDEN — your plan is below Business
- NOT_FOUND — the id does not exist, or belongs to another company
- FORBIDDEN — the key's access level is too low (for example a READ key calling a mutation)
Tip
Related articles
Was this page helpful?